Compare commits

...

4 Commits

Author SHA1 Message Date
celia-oai
918731d918 changes 2025-11-19 23:05:01 -08:00
celia-oai
037e0b5736 fix 2025-11-19 17:12:44 -08:00
Celia Chen
8e8a55723a Merge branch 'main' into cc/revert 2025-11-19 17:12:01 -08:00
celia-oai
3b27871684 Revert "[core] add optional status_code to error events (#6865)"
This reverts commit c2ec477d93.
2025-11-19 17:02:51 -08:00
14 changed files with 82 additions and 82 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -1433,6 +1433,7 @@ dependencies = [
"strum_macros 0.27.2",
"sys-locale",
"tempfile",
"thiserror 2.0.17",
"tracing",
"ts-rs",
"uuid",

View File

@@ -84,9 +84,9 @@ wildmatch = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
keyring = { workspace = true, features = ["linux-native-async-persistent"] }
landlock = { workspace = true }
seccompiler = { workspace = true }
keyring = { workspace = true, features = ["linux-native-async-persistent"] }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.9"

View File

@@ -79,7 +79,9 @@ use crate::protocol::AgentReasoningSectionBreakEvent;
use crate::protocol::ApplyPatchApprovalRequestEvent;
use crate::protocol::AskForApproval;
use crate::protocol::BackgroundEventEvent;
use crate::protocol::CodexErrorCode;
use crate::protocol::DeprecationNoticeEvent;
use crate::protocol::ErrorEvent;
use crate::protocol::Event;
use crate::protocol::EventMsg;
use crate::protocol::ExecApprovalRequestEvent;
@@ -1199,9 +1201,12 @@ impl Session {
message: impl Into<String>,
http_status_code: Option<StatusCode>,
) {
let codex_error_code = CodexErrorCode::ResponseStreamError {
http_status_code: http_status_code_value(http_status_code),
};
let event = EventMsg::StreamError(StreamErrorEvent {
message: message.into(),
http_status_code: http_status_code_value(http_status_code),
codex_error_code: Some(codex_error_code),
});
self.send_event(turn_context, event).await;
}
@@ -1693,7 +1698,6 @@ mod handlers {
id: sub_id.clone(),
msg: EventMsg::Error(ErrorEvent {
message: "Failed to shutdown rollout recorder".to_string(),
http_status_code: None,
}),
};
sess.send_event_raw(event).await;
@@ -1948,8 +1952,10 @@ pub(crate) async fn run_task(
}
Err(e) => {
info!("Turn error: {e:#}");
sess.send_event(&turn_context, EventMsg::Error(e.to_error_event(None)))
.await;
let event = EventMsg::Error(ErrorEvent {
message: e.to_string(),
});
sess.send_event(&turn_context, event).await;
// let the user continue the conversation
break;
}
@@ -2073,7 +2079,6 @@ async fn run_turn(
sess.notify_stream_error(
&turn_context,
format!("Reconnecting... {retries}/{max_retries}"),
e.http_status_code(),
)
.await;

View File

@@ -10,6 +10,7 @@ use crate::error::Result as CodexResult;
use crate::features::Feature;
use crate::protocol::AgentMessageEvent;
use crate::protocol::CompactedItem;
use crate::protocol::ErrorEvent;
use crate::protocol::EventMsg;
use crate::protocol::TaskStartedEvent;
use crate::protocol::TurnContextItem;
@@ -127,8 +128,10 @@ async fn run_compact_task_inner(
continue;
}
sess.set_total_tokens_full(turn_context.as_ref()).await;
sess.send_event(&turn_context, EventMsg::Error(e.to_error_event(None)))
.await;
let event = EventMsg::Error(ErrorEvent {
message: e.to_string(),
});
sess.send_event(&turn_context, event).await;
return;
}
Err(e) => {
@@ -138,14 +141,15 @@ async fn run_compact_task_inner(
sess.notify_stream_error(
turn_context.as_ref(),
format!("Reconnecting... {retries}/{max_retries}"),
e.http_status_code(),
)
.await;
tokio::time::sleep(delay).await;
continue;
} else {
sess.send_event(&turn_context, EventMsg::Error(e.to_error_event(None)))
.await;
let event = EventMsg::Error(ErrorEvent {
message: e.to_string(),
});
sess.send_event(&turn_context, event).await;
return;
}
}

View File

@@ -6,6 +6,7 @@ use crate::codex::TurnContext;
use crate::error::Result as CodexResult;
use crate::protocol::AgentMessageEvent;
use crate::protocol::CompactedItem;
use crate::protocol::ErrorEvent;
use crate::protocol::EventMsg;
use crate::protocol::RolloutItem;
use crate::protocol::TaskStartedEvent;
@@ -29,8 +30,10 @@ pub(crate) async fn run_remote_compact_task(sess: Arc<Session>, turn_context: Ar
async fn run_remote_compact_task_inner(sess: &Arc<Session>, turn_context: &Arc<TurnContext>) {
if let Err(err) = run_remote_compact_task_inner_impl(sess, turn_context).await {
let event = err.to_error_event(Some("Error running remote compact task".to_string()));
sess.send_event(turn_context, EventMsg::Error(event)).await;
let event = EventMsg::Error(ErrorEvent {
message: format!("Error running remote compact task: {err}"),
});
sess.send_event(turn_context, event).await;
}
}

View File

@@ -10,6 +10,7 @@ use chrono::Local;
use chrono::Utc;
use codex_async_utils::CancelErr;
use codex_protocol::ConversationId;
use codex_protocol::protocol::CodexErrorCode;
use codex_protocol::protocol::ErrorEvent;
use codex_protocol::protocol::RateLimitSnapshot;
use reqwest::StatusCode;
@@ -432,17 +433,31 @@ impl CodexErr {
(self as &dyn std::any::Any).downcast_ref::<T>()
}
pub fn http_status_code(&self) -> Option<StatusCode> {
/// Translate core error to client-facing protocol error.
pub fn to_codex_protocol_error(&self) -> CodexErrorCode {
match self {
CodexErr::UnexpectedStatus(err) => Some(err.status),
CodexErr::RetryLimit(err) => Some(err.status),
CodexErr::UsageLimitReached(_) | CodexErr::UsageNotIncluded => {
Some(StatusCode::TOO_MANY_REQUESTS)
CodexErr::ContextWindowExceeded => CodexErrorCode::ContextWindowExceeded,
CodexErr::UsageLimitReached(_)
| CodexErr::QuotaExceeded
| CodexErr::UsageNotIncluded => CodexErrorCode::UsageLimitExceeded,
CodexErr::RetryLimit(err) => CodexErrorCode::HttpRetryLimitExceeded {
http_status_code: http_status_code_value(Some(err.status)),
},
CodexErr::ConnectionFailed(err) => CodexErrorCode::HttpConnectionFailed {
http_status_code: http_status_code_value(err.source.status()),
},
CodexErr::ResponseStreamFailed(err) => CodexErrorCode::ResponseSseStreamFailed {
http_status_code: http_status_code_value(err.source.status()),
},
CodexErr::RefreshTokenFailed(_) => CodexErrorCode::Unauthorized,
CodexErr::SessionConfiguredNotFirstEvent
| CodexErr::InternalServerError
| CodexErr::InternalAgentDied => CodexErrorCode::InternalServerError,
CodexErr::UnsupportedOperation(_) | CodexErr::ConversationNotFound(_) => {
CodexErrorCode::BadRequest
}
CodexErr::InternalServerError => Some(StatusCode::INTERNAL_SERVER_ERROR),
CodexErr::ResponseStreamFailed(err) => err.source.status(),
CodexErr::ConnectionFailed(err) => err.source.status(),
_ => None,
CodexErr::Sandbox(_) => CodexErrorCode::Sandbox,
_ => CodexErrorCode::Other,
}
}
@@ -452,10 +467,9 @@ impl CodexErr {
Some(prefix) => format!("{prefix}: {error_message}"),
None => error_message,
};
ErrorEvent {
message,
http_status_code: http_status_code_value(self.http_status_code()),
codex_error_code: self.to_codex_protocol_error(),
}
}
}
@@ -807,43 +821,4 @@ mod tests {
assert_eq!(err.to_string(), expected);
});
}
#[test]
fn error_event_includes_http_status_code_when_available() {
let err = CodexErr::UnexpectedStatus(UnexpectedResponseError {
status: StatusCode::BAD_REQUEST,
body: "oops".to_string(),
request_id: Some("req-1".to_string()),
});
let event = err.to_error_event(None);
assert_eq!(
event.message,
"unexpected status 400 Bad Request: oops, request id: req-1"
);
assert_eq!(
event.http_status_code,
Some(StatusCode::BAD_REQUEST.as_u16())
);
}
#[test]
fn error_event_omits_http_status_code_when_unknown() {
let event = CodexErr::Fatal("boom".to_string()).to_error_event(None);
assert_eq!(event.message, "Fatal error: boom");
assert_eq!(event.http_status_code, None);
}
#[test]
fn error_event_applies_message_wrapper() {
let event = CodexErr::Fatal("boom".to_string())
.to_error_event(Some("Error running remote compact task".to_string()));
assert_eq!(
event.message,
"Error running remote compact task: Fatal error: boom"
);
assert_eq!(event.http_status_code, None);
}
}

View File

@@ -72,7 +72,7 @@ For complete documentation of the `Op` and `EventMsg` variants, refer to [protoc
- `EventMsg::AgentMessage` Messages from the `Model`
- `EventMsg::ExecApprovalRequest` Request approval from user to execute a command
- `EventMsg::TaskComplete` A task completed successfully
- `EventMsg::Error` A task stopped with an error (includes an optional `http_status_code` when available)
- `EventMsg::Error` A task stopped with an error
- `EventMsg::Warning` A non-fatal warning that the client should surface to the user
- `EventMsg::TurnComplete` Contains a `response_id` bookmark for last `response_id` executed by the task. This can be used to continue the task at a later point in time, perhaps with additional user input.

View File

@@ -161,7 +161,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
fn process_event(&mut self, event: Event) -> CodexStatus {
let Event { id: _, msg } = event;
match msg {
EventMsg::Error(ErrorEvent { message, .. }) => {
EventMsg::Error(ErrorEvent { message }) => {
let prefix = "ERROR:".style(self.red);
ts_msg!(self, "{prefix} {message}");
}
@@ -221,7 +221,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => {
ts_msg!(self, "{}", message.style(self.dimmed));
}
EventMsg::StreamError(StreamErrorEvent { message, .. }) => {
EventMsg::StreamError(StreamErrorEvent { message }) => {
ts_msg!(self, "{}", message.style(self.dimmed));
}
EventMsg::TaskStarted(_) => {

View File

@@ -539,7 +539,6 @@ fn error_event_produces_error() {
"e1",
EventMsg::Error(codex_core::protocol::ErrorEvent {
message: "boom".to_string(),
http_status_code: Some(500),
}),
));
assert_eq!(
@@ -579,7 +578,6 @@ fn stream_error_event_produces_error() {
"e1",
EventMsg::StreamError(codex_core::protocol::StreamErrorEvent {
message: "retrying".to_string(),
http_status_code: Some(500),
}),
));
assert_eq!(
@@ -598,7 +596,6 @@ fn error_followed_by_task_complete_produces_turn_failed() {
"e1",
EventMsg::Error(ErrorEvent {
message: "boom".to_string(),
http_status_code: Some(500),
}),
);
assert_eq!(

View File

@@ -27,6 +27,7 @@ serde_with = { workspace = true, features = ["macros", "base64"] }
strum = { workspace = true }
strum_macros = { workspace = true }
sys-locale = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
ts-rs = { workspace = true, features = [
"uuid-impl",

View File

@@ -32,6 +32,7 @@ use serde::Serialize;
use serde_json::Value;
use serde_with::serde_as;
use strum_macros::Display;
use thiserror::Error;
use ts_rs::TS;
pub use crate::approvals::ApplyPatchApprovalRequestEvent;
@@ -562,6 +563,27 @@ pub enum EventMsg {
ReasoningRawContentDelta(ReasoningRawContentDeltaEvent),
}
/// Codex errors that we expose to clients.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum CodexErrorCode {
ContextWindowExceeded,
UsageLimitExceeded,
// Exceeded the retry limit for http requests for retryable HTTP errors.
HttpRetryLimitExceeded { http_status_code: Option<u16> },
HttpConnectionFailed { http_status_code: Option<u16> },
// The SSE stream for the response failed.
ResponseSseStreamFailed { http_status_code: Option<u16> },
InternalServerError,
Unauthorized,
BadRequest,
Sandbox,
// Error emitted by response stream, Usually during retries of a retryable HTTP error.
ResponseStreamError { http_status_code: Option<u16> },
Other,
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
pub struct RawResponseItemEvent {
pub item: ResponseItem,
@@ -686,8 +708,7 @@ pub struct ExitedReviewModeEvent {
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ErrorEvent {
pub message: String,
#[serde(default)]
pub http_status_code: Option<u16>,
pub codex_error_code: Option<CodexErrorCode>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
@@ -1365,8 +1386,7 @@ pub struct UndoCompletedEvent {
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct StreamErrorEvent {
pub message: String,
#[serde(default)]
pub http_status_code: Option<u16>,
pub codex_error_code: Option<CodexErrorCode>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]

View File

@@ -1654,7 +1654,7 @@ impl ChatWidget {
self.on_rate_limit_snapshot(ev.rate_limits);
}
EventMsg::Warning(WarningEvent { message }) => self.on_warning(message),
EventMsg::Error(ErrorEvent { message, .. }) => self.on_error(message),
EventMsg::Error(ErrorEvent { message }) => self.on_error(message),
EventMsg::McpStartupUpdate(ev) => self.on_mcp_startup_update(ev),
EventMsg::McpStartupComplete(ev) => self.on_mcp_startup_complete(ev),
EventMsg::TurnAborted(ev) => match ev.reason {
@@ -1697,9 +1697,7 @@ impl ChatWidget {
}
EventMsg::UndoStarted(ev) => self.on_undo_started(ev),
EventMsg::UndoCompleted(ev) => self.on_undo_completed(ev),
EventMsg::StreamError(StreamErrorEvent { message, .. }) => {
self.on_stream_error(message)
}
EventMsg::StreamError(StreamErrorEvent { message }) => self.on_stream_error(message),
EventMsg::UserMessage(ev) => {
if from_replay {
self.on_user_message_event(ev);

View File

@@ -37,10 +37,7 @@ pub(crate) fn spawn_agent(
eprintln!("{message}");
app_event_tx_clone.send(AppEvent::CodexEvent(Event {
id: "".to_string(),
msg: EventMsg::Error(ErrorEvent {
message,
http_status_code: None,
}),
msg: EventMsg::Error(ErrorEvent { message }),
}));
app_event_tx_clone.send(AppEvent::ExitRequest);
tracing::error!("failed to initialize codex: {err}");

View File

@@ -2596,7 +2596,6 @@ fn stream_error_updates_status_indicator() {
id: "sub-1".into(),
msg: EventMsg::StreamError(StreamErrorEvent {
message: msg.to_string(),
http_status_code: None,
}),
});