mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Separate Codex error details from retry metadata (#34996)
## What changed - Wrap `CodexErrorDetails` and an optional retry delay in `CodexErr`, allowing any mapped error to preserve server-provided retry timing. - Generate the payload-free `CodexErrKind` classification alongside the error details and reuse it for analytics. - Update error handling sites to inspect `CodexErr::details()` while preserving existing display, debug, protocol mapping, and retryability behavior. ## Testing - Add coverage for legacy debug formatting, error-specific retryability, and retry-delay propagation through API error mapping. GitOrigin-RevId: d3ab8a305f2a2ee21d0c0a8c8c388b06dda9c59a
This commit is contained in:
committed by
copyberry
parent
265cd2e100
commit
7bafdada8b
@@ -16,6 +16,7 @@ use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::error::CodexErr;
|
||||
pub use codex_protocol::error::CodexErrKind;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
@@ -139,48 +140,6 @@ impl TurnCodexErrorFact {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CodexErrKind {
|
||||
TurnAborted,
|
||||
SessionBudgetExceeded,
|
||||
Stream,
|
||||
ContextWindowExceeded,
|
||||
ThreadNotFound,
|
||||
AgentLimitReached,
|
||||
SessionConfiguredNotFirstEvent,
|
||||
Timeout,
|
||||
RequestTimeout,
|
||||
Spawn,
|
||||
Interrupted,
|
||||
UnexpectedStatus,
|
||||
InvalidRequest,
|
||||
InvalidImageRequest,
|
||||
UsageLimitReached,
|
||||
ServerOverloaded,
|
||||
CyberPolicy,
|
||||
ResponseStreamFailed,
|
||||
ConnectionFailed,
|
||||
QuotaExceeded,
|
||||
UsageNotIncluded,
|
||||
InternalServerError,
|
||||
RetryLimit,
|
||||
InternalAgentDied,
|
||||
Sandbox,
|
||||
LandlockSandboxExecutableNotProvided,
|
||||
UnsupportedOperation,
|
||||
RefreshTokenFailed,
|
||||
Fatal,
|
||||
Io,
|
||||
Json,
|
||||
#[cfg(target_os = "linux")]
|
||||
LandlockRuleset,
|
||||
#[cfg(target_os = "linux")]
|
||||
LandlockPathFd,
|
||||
TokioJoin,
|
||||
EnvVar,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TurnCodexError {
|
||||
pub(crate) kind: CodexErrKind,
|
||||
@@ -196,54 +155,6 @@ impl TurnCodexError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CodexErr> for CodexErrKind {
|
||||
fn from(error: &CodexErr) -> Self {
|
||||
match error {
|
||||
CodexErr::TurnAborted => CodexErrKind::TurnAborted,
|
||||
CodexErr::SessionBudgetExceeded => CodexErrKind::SessionBudgetExceeded,
|
||||
CodexErr::Stream(..) => CodexErrKind::Stream,
|
||||
CodexErr::ContextWindowExceeded => CodexErrKind::ContextWindowExceeded,
|
||||
CodexErr::ThreadNotFound(_) => CodexErrKind::ThreadNotFound,
|
||||
CodexErr::AgentLimitReached { .. } => CodexErrKind::AgentLimitReached,
|
||||
CodexErr::SessionConfiguredNotFirstEvent => {
|
||||
CodexErrKind::SessionConfiguredNotFirstEvent
|
||||
}
|
||||
CodexErr::Timeout => CodexErrKind::Timeout,
|
||||
CodexErr::RequestTimeout => CodexErrKind::RequestTimeout,
|
||||
CodexErr::Spawn => CodexErrKind::Spawn,
|
||||
CodexErr::Interrupted => CodexErrKind::Interrupted,
|
||||
CodexErr::UnexpectedStatus(_) => CodexErrKind::UnexpectedStatus,
|
||||
CodexErr::InvalidRequest(_) => CodexErrKind::InvalidRequest,
|
||||
CodexErr::InvalidImageRequest() => CodexErrKind::InvalidImageRequest,
|
||||
CodexErr::UsageLimitReached(_) => CodexErrKind::UsageLimitReached,
|
||||
CodexErr::ServerOverloaded => CodexErrKind::ServerOverloaded,
|
||||
CodexErr::CyberPolicy { .. } => CodexErrKind::CyberPolicy,
|
||||
CodexErr::ResponseStreamFailed(_) => CodexErrKind::ResponseStreamFailed,
|
||||
CodexErr::ConnectionFailed(_) => CodexErrKind::ConnectionFailed,
|
||||
CodexErr::QuotaExceeded => CodexErrKind::QuotaExceeded,
|
||||
CodexErr::UsageNotIncluded => CodexErrKind::UsageNotIncluded,
|
||||
CodexErr::InternalServerError => CodexErrKind::InternalServerError,
|
||||
CodexErr::RetryLimit(_) => CodexErrKind::RetryLimit,
|
||||
CodexErr::InternalAgentDied => CodexErrKind::InternalAgentDied,
|
||||
CodexErr::Sandbox(_) => CodexErrKind::Sandbox,
|
||||
CodexErr::LandlockSandboxExecutableNotProvided => {
|
||||
CodexErrKind::LandlockSandboxExecutableNotProvided
|
||||
}
|
||||
CodexErr::UnsupportedOperation(_) => CodexErrKind::UnsupportedOperation,
|
||||
CodexErr::RefreshTokenFailed(_) => CodexErrKind::RefreshTokenFailed,
|
||||
CodexErr::Fatal(_) => CodexErrKind::Fatal,
|
||||
CodexErr::Io(_) => CodexErrKind::Io,
|
||||
CodexErr::Json(_) => CodexErrKind::Json,
|
||||
#[cfg(target_os = "linux")]
|
||||
CodexErr::LandlockRuleset(_) => CodexErrKind::LandlockRuleset,
|
||||
#[cfg(target_os = "linux")]
|
||||
CodexErr::LandlockPathFd(_) => CodexErrKind::LandlockPathFd,
|
||||
CodexErr::TokioJoin(_) => CodexErrKind::TokioJoin,
|
||||
CodexErr::EnvVar(_) => CodexErrKind::EnvVar,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TurnStatus {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use super::*;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
|
||||
pub(super) fn environment_selection_error(err: CodexErr) -> JSONRPCErrorError {
|
||||
match err {
|
||||
CodexErr::InvalidRequest(message) => invalid_request(message),
|
||||
err => internal_error(format!("failed to validate environment selections: {err}")),
|
||||
match err.details() {
|
||||
CodexErrorDetails::InvalidRequest(message) => invalid_request(message.clone()),
|
||||
_ => internal_error(format!("failed to validate environment selections: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::error_code::method_not_found;
|
||||
use codex_app_server_protocol::SelectedCapabilityRoot;
|
||||
use codex_extension_api::ExtensionDataInit;
|
||||
use codex_protocol::config_types::MultiAgentMode;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
||||
use codex_protocol::protocol::ThreadHistoryMode;
|
||||
@@ -1253,10 +1254,12 @@ impl ThreadRequestProcessor {
|
||||
thread_start.dynamic_tool_count = dynamic_tool_count,
|
||||
))
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
CodexErr::InvalidRequest(message) => invalid_request(message),
|
||||
CodexErr::UnsupportedOperation(message) => method_not_found(message),
|
||||
err => internal_error(format!("error creating thread: {err}")),
|
||||
.map_err(|err| match err.details() {
|
||||
CodexErrorDetails::InvalidRequest(message) => invalid_request(message.clone()),
|
||||
CodexErrorDetails::UnsupportedOperation(message) => {
|
||||
method_not_found(message.clone())
|
||||
}
|
||||
_ => internal_error(format!("error creating thread: {err}")),
|
||||
})?;
|
||||
let session_telemetry = thread.session_telemetry();
|
||||
session_telemetry.record_startup_phase(
|
||||
@@ -1553,9 +1556,9 @@ impl ThreadRequestProcessor {
|
||||
let count = thread
|
||||
.decrement_out_of_band_elicitation_count()
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
CodexErr::InvalidRequest(message) => invalid_request(message),
|
||||
err => internal_error(format!(
|
||||
.map_err(|err| match err.details() {
|
||||
CodexErrorDetails::InvalidRequest(message) => invalid_request(message.clone()),
|
||||
_ => internal_error(format!(
|
||||
"failed to decrement out-of-band elicitation counter: {err}"
|
||||
)),
|
||||
})?;
|
||||
@@ -3399,9 +3402,9 @@ impl ThreadRequestProcessor {
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
let error = match err {
|
||||
CodexErr::InvalidRequest(message) => invalid_request(message),
|
||||
err => internal_error(format!("error resuming thread: {err}")),
|
||||
let error = match err.details() {
|
||||
CodexErrorDetails::InvalidRequest(message) => invalid_request(message.clone()),
|
||||
_ => internal_error(format!("error resuming thread: {err}")),
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
}
|
||||
@@ -4129,12 +4132,12 @@ impl ThreadRequestProcessor {
|
||||
supports_openai_form_elicitation,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
CodexErr::Io(_) | CodexErr::Json(_) => {
|
||||
.map_err(|err| match err.details() {
|
||||
CodexErrorDetails::Io(_) | CodexErrorDetails::Json(_) => {
|
||||
invalid_request(format!("failed to load thread {source_thread_id}: {err}"))
|
||||
}
|
||||
CodexErr::InvalidRequest(message) => invalid_request(message),
|
||||
err => internal_error(format!("error forking thread: {err}")),
|
||||
CodexErrorDetails::InvalidRequest(message) => invalid_request(message.clone()),
|
||||
_ => internal_error(format!("error forking thread: {err}")),
|
||||
})?;
|
||||
|
||||
Self::set_app_server_client_info(
|
||||
@@ -4932,13 +4935,13 @@ fn conversation_summary_rollout_path_read_error(
|
||||
}
|
||||
|
||||
pub(super) fn core_thread_write_error(operation: &str, err: CodexErr) -> JSONRPCErrorError {
|
||||
match err {
|
||||
CodexErr::ThreadNotFound(thread_id) => {
|
||||
match err.details() {
|
||||
CodexErrorDetails::ThreadNotFound(thread_id) => {
|
||||
invalid_request(format!("thread not found: {thread_id}"))
|
||||
}
|
||||
CodexErr::InvalidRequest(message) => invalid_request(message),
|
||||
CodexErr::UnsupportedOperation(message) => method_not_found(message),
|
||||
err => internal_error(format!("failed to {operation}: {err}")),
|
||||
CodexErrorDetails::InvalidRequest(message) => invalid_request(message.clone()),
|
||||
CodexErrorDetails::UnsupportedOperation(message) => method_not_found(message.clone()),
|
||||
_ => internal_error(format!("failed to {operation}: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::*;
|
||||
use codex_agent_extension::AgentInvocation;
|
||||
use codex_agent_extension::AgentRun;
|
||||
use codex_agent_extension::AgentRunner;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
@@ -890,9 +891,9 @@ impl TurnRequestProcessor {
|
||||
thread
|
||||
.inject_response_items(items)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
CodexErr::InvalidRequest(message) => invalid_request(message),
|
||||
err => internal_error(format!("failed to inject response items: {err}")),
|
||||
.map_err(|err| match err.details() {
|
||||
CodexErrorDetails::InvalidRequest(message) => invalid_request(message.clone()),
|
||||
_ => internal_error(format!("failed to inject response items: {err}")),
|
||||
})?;
|
||||
Ok(ThreadInjectItemsResponse {})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use codex_protocol::auth::PlanType;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::error::RetryLimitReachedError;
|
||||
use codex_protocol::error::UnexpectedResponseError;
|
||||
use codex_protocol::error::UsageLimitReachedError;
|
||||
@@ -20,8 +21,14 @@ pub fn map_api_error(err: ApiError) -> CodexErr {
|
||||
ApiError::ContextWindowExceeded => CodexErr::ContextWindowExceeded,
|
||||
ApiError::QuotaExceeded => CodexErr::QuotaExceeded,
|
||||
ApiError::UsageNotIncluded => CodexErr::UsageNotIncluded,
|
||||
ApiError::Retryable { message, delay } => CodexErr::Stream(message, delay),
|
||||
ApiError::Stream(msg) => CodexErr::Stream(msg, None),
|
||||
ApiError::Retryable { message, delay } => {
|
||||
let error = CodexErr::Stream(message);
|
||||
match delay {
|
||||
Some(delay) => error.with_retry_delay(delay),
|
||||
None => error,
|
||||
}
|
||||
}
|
||||
ApiError::Stream(msg) => CodexErr::Stream(msg),
|
||||
ApiError::ServerOverloaded => CodexErr::ServerOverloaded,
|
||||
ApiError::Api { status, message } => {
|
||||
let user_message = api_error_user_message(status, &message);
|
||||
@@ -37,7 +44,9 @@ pub fn map_api_error(err: ApiError) -> CodexErr {
|
||||
})
|
||||
}
|
||||
ApiError::InvalidRequest { message } => CodexErr::InvalidRequest(message),
|
||||
ApiError::CyberPolicy { message } => CodexErr::CyberPolicy { message },
|
||||
ApiError::CyberPolicy { message } => {
|
||||
CodexErr::new(CodexErrorDetails::CyberPolicy { message })
|
||||
}
|
||||
ApiError::Transport(transport) => match transport {
|
||||
TransportError::Http {
|
||||
status,
|
||||
@@ -72,7 +81,7 @@ pub fn map_api_error(err: ApiError) -> CodexErr {
|
||||
.filter(|message| !message.trim().is_empty())
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| CYBER_POLICY_FALLBACK_MESSAGE.to_string());
|
||||
CodexErr::CyberPolicy { message }
|
||||
CodexErr::new(CodexErrorDetails::CyberPolicy { message })
|
||||
} else if body_text
|
||||
.contains("The image data you provided does not represent a valid image")
|
||||
{
|
||||
@@ -139,11 +148,9 @@ pub fn map_api_error(err: ApiError) -> CodexErr {
|
||||
request_id: None,
|
||||
}),
|
||||
TransportError::Timeout => CodexErr::RequestTimeout,
|
||||
TransportError::Network(msg) | TransportError::Build(msg) => {
|
||||
CodexErr::Stream(msg, None)
|
||||
}
|
||||
TransportError::Network(msg) | TransportError::Build(msg) => CodexErr::Stream(msg),
|
||||
},
|
||||
ApiError::RateLimit(msg) => CodexErr::Stream(msg, None),
|
||||
ApiError::RateLimit(msg) => CodexErr::Stream(msg),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,22 @@ use pretty_assertions::assert_eq;
|
||||
#[test]
|
||||
fn map_api_error_maps_server_overloaded() {
|
||||
let err = map_api_error(ApiError::ServerOverloaded);
|
||||
assert!(matches!(err, CodexErr::ServerOverloaded));
|
||||
assert!(matches!(err.details(), CodexErrorDetails::ServerOverloaded));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_api_error_preserves_retry_delay() {
|
||||
let retry_delay = std::time::Duration::from_secs(17);
|
||||
let err = map_api_error(ApiError::Retryable {
|
||||
message: "retry later".to_string(),
|
||||
delay: Some(retry_delay),
|
||||
});
|
||||
|
||||
assert!(matches!(
|
||||
err.details(),
|
||||
CodexErrorDetails::Stream(message) if message == "retry later"
|
||||
));
|
||||
assert_eq!(err.retry_delay(), Some(retry_delay));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -24,7 +39,7 @@ fn map_api_error_maps_server_overloaded_from_503_body() {
|
||||
body: Some(body),
|
||||
}));
|
||||
|
||||
assert!(matches!(err, CodexErr::ServerOverloaded));
|
||||
assert!(matches!(err.details(), CodexErrorDetails::ServerOverloaded));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -40,8 +55,8 @@ fn map_api_error_maps_cloudflare_blocked_response_to_user_message() {
|
||||
),
|
||||
}));
|
||||
|
||||
let CodexErr::UnexpectedStatus(err) = err else {
|
||||
panic!("expected CodexErr::UnexpectedStatus, got {err:?}");
|
||||
let CodexErrorDetails::UnexpectedStatus(err) = err.details() else {
|
||||
panic!("expected CodexErrorDetails::UnexpectedStatus, got {err:?}");
|
||||
};
|
||||
assert_eq!(
|
||||
err.user_message.as_deref(),
|
||||
@@ -73,8 +88,8 @@ fn map_api_error_maps_cyber_policy_from_400_body() {
|
||||
body: Some(body),
|
||||
}));
|
||||
|
||||
let CodexErr::CyberPolicy { message } = err else {
|
||||
panic!("expected CodexErr::CyberPolicy, got {err:?}");
|
||||
let CodexErrorDetails::CyberPolicy { message } = err.details() else {
|
||||
panic!("expected CodexErrorDetails::CyberPolicy, got {err:?}");
|
||||
};
|
||||
assert_eq!(
|
||||
message,
|
||||
@@ -101,8 +116,8 @@ fn map_api_error_maps_wrapped_websocket_cyber_policy_from_400_body() {
|
||||
body: Some(body),
|
||||
}));
|
||||
|
||||
let CodexErr::CyberPolicy { message } = err else {
|
||||
panic!("expected CodexErr::CyberPolicy, got {err:?}");
|
||||
let CodexErrorDetails::CyberPolicy { message } = err.details() else {
|
||||
panic!("expected CodexErrorDetails::CyberPolicy, got {err:?}");
|
||||
};
|
||||
assert_eq!(message, "This websocket request was flagged.");
|
||||
}
|
||||
@@ -122,8 +137,8 @@ fn map_api_error_uses_cyber_policy_fallback_for_missing_message() {
|
||||
body: Some(body),
|
||||
}));
|
||||
|
||||
let CodexErr::CyberPolicy { message } = err else {
|
||||
panic!("expected CodexErr::CyberPolicy, got {err:?}");
|
||||
let CodexErrorDetails::CyberPolicy { message } = err.details() else {
|
||||
panic!("expected CodexErrorDetails::CyberPolicy, got {err:?}");
|
||||
};
|
||||
assert_eq!(
|
||||
message,
|
||||
@@ -147,10 +162,10 @@ fn map_api_error_keeps_unknown_400_errors_generic() {
|
||||
body: Some(body.clone()),
|
||||
}));
|
||||
|
||||
let CodexErr::InvalidRequest(message) = err else {
|
||||
panic!("expected CodexErr::InvalidRequest, got {err:?}");
|
||||
let CodexErrorDetails::InvalidRequest(message) = err.details() else {
|
||||
panic!("expected CodexErrorDetails::InvalidRequest, got {err:?}");
|
||||
};
|
||||
assert_eq!(message, body);
|
||||
assert_eq!(message, &body);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -178,8 +193,8 @@ fn map_api_error_maps_usage_limit_limit_name_header() {
|
||||
body: Some(body),
|
||||
}));
|
||||
|
||||
let CodexErr::UsageLimitReached(usage_limit) = err else {
|
||||
panic!("expected CodexErr::UsageLimitReached, got {err:?}");
|
||||
let CodexErrorDetails::UsageLimitReached(usage_limit) = err.details() else {
|
||||
panic!("expected CodexErrorDetails::UsageLimitReached, got {err:?}");
|
||||
};
|
||||
assert_eq!(
|
||||
usage_limit
|
||||
@@ -211,8 +226,8 @@ fn map_api_error_does_not_fallback_limit_name_to_limit_id() {
|
||||
body: Some(body),
|
||||
}));
|
||||
|
||||
let CodexErr::UsageLimitReached(usage_limit) = err else {
|
||||
panic!("expected CodexErr::UsageLimitReached, got {err:?}");
|
||||
let CodexErrorDetails::UsageLimitReached(usage_limit) = err.details() else {
|
||||
panic!("expected CodexErrorDetails::UsageLimitReached, got {err:?}");
|
||||
};
|
||||
assert_eq!(
|
||||
usage_limit
|
||||
@@ -260,8 +275,8 @@ fn map_api_error_copies_rate_limit_reached_type_to_usage_limit_snapshot() {
|
||||
body: Some(body),
|
||||
}));
|
||||
|
||||
let CodexErr::UsageLimitReached(usage_limit) = err else {
|
||||
panic!("expected CodexErr::UsageLimitReached, got {err:?}");
|
||||
let CodexErrorDetails::UsageLimitReached(usage_limit) = err.details() else {
|
||||
panic!("expected CodexErrorDetails::UsageLimitReached, got {err:?}");
|
||||
};
|
||||
assert_eq!(
|
||||
usage_limit.rate_limit_reached_type,
|
||||
@@ -311,8 +326,8 @@ fn map_api_error_ignores_unparseable_rate_limit_reached_type_headers() {
|
||||
body: Some(body),
|
||||
}));
|
||||
|
||||
let CodexErr::UsageLimitReached(usage_limit) = err else {
|
||||
panic!("expected CodexErr::UsageLimitReached, got {err:?}");
|
||||
let CodexErrorDetails::UsageLimitReached(usage_limit) = err.details() else {
|
||||
panic!("expected CodexErrorDetails::UsageLimitReached, got {err:?}");
|
||||
};
|
||||
assert_eq!(usage_limit.rate_limit_reached_type, None);
|
||||
}
|
||||
@@ -341,8 +356,8 @@ fn map_api_error_extracts_identity_auth_details_from_headers() {
|
||||
body: Some(r#"{"detail":"Unauthorized"}"#.to_string()),
|
||||
}));
|
||||
|
||||
let CodexErr::UnexpectedStatus(err) = err else {
|
||||
panic!("expected CodexErr::UnexpectedStatus, got {err:?}");
|
||||
let CodexErrorDetails::UnexpectedStatus(err) = err.details() else {
|
||||
panic!("expected CodexErrorDetails::UnexpectedStatus, got {err:?}");
|
||||
};
|
||||
assert_eq!(err.request_id.as_deref(), Some("req-401"));
|
||||
assert_eq!(err.cf_ray.as_deref(), Some("ray-401"));
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::function_tool::FunctionCallError;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Resolves a single tool-facing agent target to a thread id.
|
||||
@@ -20,11 +21,11 @@ pub(crate) async fn resolve_agent_target(
|
||||
.agent_control
|
||||
.resolve_agent_reference(session.thread_id, &turn.session_source, target)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
codex_protocol::error::CodexErr::UnsupportedOperation(message) => {
|
||||
FunctionCallError::RespondToModel(message)
|
||||
.map_err(|err| match err.details() {
|
||||
CodexErrorDetails::UnsupportedOperation(message) => {
|
||||
FunctionCallError::RespondToModel(message.clone())
|
||||
}
|
||||
other => FunctionCallError::RespondToModel(other.to_string()),
|
||||
_ => FunctionCallError::RespondToModel(err.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ use codex_protocol::AgentPath;
|
||||
use codex_protocol::SessionId;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
@@ -241,7 +242,10 @@ impl AgentControl {
|
||||
state: &Arc<ThreadManagerState>,
|
||||
result: CodexResult<String>,
|
||||
) -> CodexResult<String> {
|
||||
if matches!(result, Err(CodexErr::InternalAgentDied)) {
|
||||
if result
|
||||
.as_ref()
|
||||
.is_err_and(|err| matches!(err.details(), CodexErrorDetails::InternalAgentDied))
|
||||
{
|
||||
let _ = state.remove_thread(&agent_id).await;
|
||||
self.forget_v2_residency(agent_id);
|
||||
self.state.release_spawned_thread(agent_id);
|
||||
@@ -278,7 +282,7 @@ impl AgentControl {
|
||||
pub(crate) fn ensure_agent_known(&self, agent_id: ThreadId) -> CodexResult<AgentMetadata> {
|
||||
self.state
|
||||
.agent_metadata_for_thread(agent_id)
|
||||
.ok_or(CodexErr::ThreadNotFound(agent_id))
|
||||
.ok_or_else(|| CodexErr::ThreadNotFound(agent_id))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_live_agent_subtree_thread_ids(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::AgentControl;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_protocol::protocol::Op;
|
||||
@@ -68,7 +69,9 @@ impl AgentControl {
|
||||
if self.agent_execution_limiter.has_capacity() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CodexErr::AgentLimitReached { max_threads })
|
||||
Err(CodexErr::new(CodexErrorDetails::AgentLimitReached {
|
||||
max_threads,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::agent::AgentControl;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
@@ -29,10 +29,10 @@ fn execution_guards_count_active_v2_subagent_turns() {
|
||||
let Err(err) = control.ensure_execution_capacity(MultiAgentVersion::V2, &source) else {
|
||||
panic!("second active turn should exceed the derived non-root cap");
|
||||
};
|
||||
let CodexErr::AgentLimitReached { max_threads } = err else {
|
||||
let CodexErrorDetails::AgentLimitReached { max_threads } = err.details() else {
|
||||
panic!("expected AgentLimitReached");
|
||||
};
|
||||
assert_eq!(max_threads, 1);
|
||||
assert_eq!(*max_threads, 1);
|
||||
|
||||
drop(first);
|
||||
control
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
|
||||
impl AgentControl {
|
||||
/// Submit a shutdown request for a live agent without marking it explicitly closed in
|
||||
@@ -43,7 +44,9 @@ impl AgentControl {
|
||||
warn!("failed to persist thread-spawn edge status for {agent_id}: {err}");
|
||||
}
|
||||
}
|
||||
Err(CodexErr::ThreadNotFound(_)) if known_agent => {
|
||||
Err(err)
|
||||
if known_agent && matches!(err.details(), CodexErrorDetails::ThreadNotFound(_)) =>
|
||||
{
|
||||
if let Some(agent_graph_store) = state.agent_graph_store()
|
||||
&& let Err(err) = agent_graph_store
|
||||
.set_thread_spawn_edge_status(
|
||||
@@ -57,13 +60,19 @@ impl AgentControl {
|
||||
)));
|
||||
}
|
||||
}
|
||||
Err(CodexErr::ThreadNotFound(_)) => {}
|
||||
Err(err) if matches!(err.details(), CodexErrorDetails::ThreadNotFound(_)) => {}
|
||||
Err(err) => {
|
||||
warn!("failed to inspect agent before close {agent_id}: {err}");
|
||||
}
|
||||
}
|
||||
match Box::pin(self.shutdown_agent_tree(agent_id)).await {
|
||||
Err(CodexErr::ThreadNotFound(_)) | Err(CodexErr::InternalAgentDied) if known_agent => {
|
||||
Err(err)
|
||||
if known_agent
|
||||
&& matches!(
|
||||
err.details(),
|
||||
CodexErrorDetails::ThreadNotFound(_) | CodexErrorDetails::InternalAgentDied
|
||||
) =>
|
||||
{
|
||||
Ok(String::new())
|
||||
}
|
||||
result => result,
|
||||
@@ -76,7 +85,12 @@ impl AgentControl {
|
||||
let result = self.shutdown_live_agent(agent_id).await;
|
||||
for descendant_id in descendant_ids {
|
||||
match self.shutdown_live_agent(descendant_id).await {
|
||||
Ok(_) | Err(CodexErr::ThreadNotFound(_)) | Err(CodexErr::InternalAgentDied) => {}
|
||||
Ok(_) => {}
|
||||
Err(err)
|
||||
if matches!(
|
||||
err.details(),
|
||||
CodexErrorDetails::ThreadNotFound(_) | CodexErrorDetails::InternalAgentDied
|
||||
) => {}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::config::Config;
|
||||
use crate::thread_manager::ThreadManagerState;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
@@ -94,9 +95,9 @@ impl V2Residency {
|
||||
.try_unload_one_resident(manager, protected_thread_id)
|
||||
.await
|
||||
{
|
||||
return Err(CodexErr::AgentLimitReached {
|
||||
return Err(CodexErr::new(CodexErrorDetails::AgentLimitReached {
|
||||
max_threads: capacity,
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::thread_manager::ThreadManagerState;
|
||||
use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
@@ -54,8 +54,10 @@ async fn residency_slot_reservation_unloads_oldest_idle_v2_agent() {
|
||||
.await
|
||||
.expect("second resident slot should evict the first idle agent");
|
||||
match manager.get_thread(first.thread_id).await {
|
||||
Err(CodexErr::ThreadNotFound(thread_id)) => assert_eq!(thread_id, first.thread_id),
|
||||
Err(err) => panic!("expected evicted thread to be missing, got {err:?}"),
|
||||
Err(err) => match err.details() {
|
||||
CodexErrorDetails::ThreadNotFound(thread_id) => assert_eq!(*thread_id, first.thread_id),
|
||||
_ => panic!("expected evicted thread to be missing, got {err:?}"),
|
||||
},
|
||||
Ok(_) => panic!("expected evicted thread to be missing"),
|
||||
}
|
||||
let second = spawn_v2_subagent(&control, &state, config, root.thread_id, "worker-2").await;
|
||||
@@ -100,8 +102,10 @@ async fn interrupted_v2_agent_is_lost_after_residency_eviction() {
|
||||
.await
|
||||
.expect("second resident slot should evict the first interrupted idle agent");
|
||||
match manager.get_thread(first.thread_id).await {
|
||||
Err(CodexErr::ThreadNotFound(thread_id)) => assert_eq!(thread_id, first.thread_id),
|
||||
Err(err) => panic!("expected evicted thread to be missing, got {err:?}"),
|
||||
Err(err) => match err.details() {
|
||||
CodexErrorDetails::ThreadNotFound(thread_id) => assert_eq!(*thread_id, first.thread_id),
|
||||
_ => panic!("expected evicted thread to be missing, got {err:?}"),
|
||||
},
|
||||
Ok(_) => panic!("expected evicted thread to be missing"),
|
||||
}
|
||||
let second =
|
||||
@@ -113,16 +117,18 @@ async fn interrupted_v2_agent_is_lost_after_residency_eviction() {
|
||||
.ensure_v2_agent_loaded(config, first.thread_id)
|
||||
.await
|
||||
.expect_err("evicted interrupted agent should stay lost");
|
||||
match err {
|
||||
CodexErr::ThreadNotFound(thread_id) => assert_eq!(thread_id, first.thread_id),
|
||||
err => panic!("expected ThreadNotFound, got {err:?}"),
|
||||
match err.details() {
|
||||
CodexErrorDetails::ThreadNotFound(thread_id) => assert_eq!(*thread_id, first.thread_id),
|
||||
_ => panic!("expected ThreadNotFound, got {err:?}"),
|
||||
}
|
||||
|
||||
assert!(manager.get_thread(root.thread_id).await.is_ok());
|
||||
assert!(manager.get_thread(second.thread_id).await.is_ok());
|
||||
match manager.get_thread(first.thread_id).await {
|
||||
Err(CodexErr::ThreadNotFound(thread_id)) => assert_eq!(thread_id, first.thread_id),
|
||||
Err(err) => panic!("expected evicted thread to be missing, got {err:?}"),
|
||||
Err(err) => match err.details() {
|
||||
CodexErrorDetails::ThreadNotFound(thread_id) => assert_eq!(*thread_id, first.thread_id),
|
||||
_ => panic!("expected evicted thread to be missing, got {err:?}"),
|
||||
},
|
||||
Ok(_) => panic!("expected evicted thread to be missing"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Settings;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::items::UserMessageItem;
|
||||
use codex_protocol::models::ContentItem;
|
||||
@@ -350,8 +351,10 @@ async fn wait_for_live_thread_spawn_children(
|
||||
|
||||
async fn assert_thread_not_loaded(manager: &ThreadManager, thread_id: ThreadId) {
|
||||
match manager.get_thread(thread_id).await {
|
||||
Err(CodexErr::ThreadNotFound(id)) => assert_eq!(id, thread_id),
|
||||
Err(err) => panic!("expected ThreadNotFound, got {err:?}"),
|
||||
Err(err) => match err.details() {
|
||||
CodexErrorDetails::ThreadNotFound(id) => assert_eq!(*id, thread_id),
|
||||
_ => panic!("expected ThreadNotFound, got {err:?}"),
|
||||
},
|
||||
Ok(_) => panic!("expected thread not to be loaded"),
|
||||
}
|
||||
}
|
||||
@@ -483,7 +486,10 @@ async fn send_input_errors_when_thread_missing() {
|
||||
)
|
||||
.await
|
||||
.expect_err("send_input should fail for missing thread");
|
||||
assert_matches!(err, CodexErr::ThreadNotFound(id) if id == thread_id);
|
||||
assert_matches!(
|
||||
err.details(),
|
||||
CodexErrorDetails::ThreadNotFound(id) if *id == thread_id
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -510,7 +516,10 @@ async fn subscribe_status_errors_for_missing_thread() {
|
||||
.subscribe_status(thread_id)
|
||||
.await
|
||||
.expect_err("subscribe_status should fail for missing thread");
|
||||
assert_matches!(err, CodexErr::ThreadNotFound(id) if id == thread_id);
|
||||
assert_matches!(
|
||||
err.details(),
|
||||
CodexErrorDetails::ThreadNotFound(id) if *id == thread_id
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -689,8 +698,10 @@ async fn ensure_v2_agent_loaded_reloads_registered_unloaded_agent() {
|
||||
.is_some()
|
||||
);
|
||||
match harness.manager.get_thread(spawned_agent.thread_id).await {
|
||||
Err(CodexErr::ThreadNotFound(id)) => assert_eq!(id, spawned_agent.thread_id),
|
||||
Err(err) => panic!("expected ThreadNotFound, got {err:?}"),
|
||||
Err(err) => match err.details() {
|
||||
CodexErrorDetails::ThreadNotFound(id) => assert_eq!(*id, spawned_agent.thread_id),
|
||||
_ => panic!("expected ThreadNotFound, got {err:?}"),
|
||||
},
|
||||
Ok(_) => panic!("expected thread to be removed"),
|
||||
}
|
||||
|
||||
@@ -1978,13 +1989,13 @@ async fn spawn_agent_respects_legacy_max_threads_alias() {
|
||||
)
|
||||
.await
|
||||
.expect_err("spawn_agent should respect max threads");
|
||||
let CodexErr::AgentLimitReached {
|
||||
let CodexErrorDetails::AgentLimitReached {
|
||||
max_threads: seen_max_threads,
|
||||
} = err
|
||||
} = err.details()
|
||||
else {
|
||||
panic!("expected CodexErr::AgentLimitReached");
|
||||
panic!("expected AgentLimitReached");
|
||||
};
|
||||
assert_eq!(seen_max_threads, max_threads);
|
||||
assert_eq!(*seen_max_threads, max_threads);
|
||||
|
||||
let _ = control
|
||||
.shutdown_live_agent(first_agent_id)
|
||||
@@ -2069,10 +2080,10 @@ async fn spawn_agent_limit_shared_across_clones() {
|
||||
)
|
||||
.await
|
||||
.expect_err("spawn_agent should respect shared guard");
|
||||
let CodexErr::AgentLimitReached { max_threads } = err else {
|
||||
panic!("expected CodexErr::AgentLimitReached");
|
||||
let CodexErrorDetails::AgentLimitReached { max_threads } = err.details() else {
|
||||
panic!("expected AgentLimitReached");
|
||||
};
|
||||
assert_eq!(max_threads, 1);
|
||||
assert_eq!(*max_threads, 1);
|
||||
|
||||
let _ = control
|
||||
.shutdown_live_agent(first_agent_id)
|
||||
@@ -2122,13 +2133,13 @@ async fn resume_agent_respects_max_threads_limit() {
|
||||
.resume_agent_from_rollout(config, resumable_id, SessionSource::Exec)
|
||||
.await
|
||||
.expect_err("resume should respect max threads");
|
||||
let CodexErr::AgentLimitReached {
|
||||
let CodexErrorDetails::AgentLimitReached {
|
||||
max_threads: seen_max_threads,
|
||||
} = err
|
||||
} = err.details()
|
||||
else {
|
||||
panic!("expected CodexErr::AgentLimitReached");
|
||||
panic!("expected AgentLimitReached");
|
||||
};
|
||||
assert_eq!(seen_max_threads, max_threads);
|
||||
assert_eq!(*seen_max_threads, max_threads);
|
||||
|
||||
let _ = control
|
||||
.shutdown_live_agent(active_id)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::error::Result;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
@@ -82,7 +83,9 @@ impl AgentRegistry {
|
||||
) -> Result<SpawnReservation> {
|
||||
if let Some(max_threads) = max_threads {
|
||||
if !self.try_increment_spawned(max_threads) {
|
||||
return Err(CodexErr::AgentLimitReached { max_threads });
|
||||
return Err(CodexErr::new(CodexErrorDetails::AgentLimitReached {
|
||||
max_threads,
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
self.total_count.fetch_add(1, Ordering::AcqRel);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashSet;
|
||||
|
||||
@@ -91,10 +92,10 @@ fn commit_holds_slot_until_release() {
|
||||
Ok(_) => panic!("limit should be enforced"),
|
||||
Err(err) => err,
|
||||
};
|
||||
let CodexErr::AgentLimitReached { max_threads } = err else {
|
||||
panic!("expected CodexErr::AgentLimitReached");
|
||||
let CodexErrorDetails::AgentLimitReached { max_threads } = err.details() else {
|
||||
panic!("expected AgentLimitReached");
|
||||
};
|
||||
assert_eq!(max_threads, 1);
|
||||
assert_eq!(*max_threads, 1);
|
||||
|
||||
registry.release_spawned_thread(thread_id);
|
||||
let reservation = registry
|
||||
@@ -116,10 +117,10 @@ fn release_ignores_unknown_thread_id() {
|
||||
Ok(_) => panic!("limit should still be enforced"),
|
||||
Err(err) => err,
|
||||
};
|
||||
let CodexErr::AgentLimitReached { max_threads } = err else {
|
||||
panic!("expected CodexErr::AgentLimitReached");
|
||||
let CodexErrorDetails::AgentLimitReached { max_threads } = err.details() else {
|
||||
panic!("expected AgentLimitReached");
|
||||
};
|
||||
assert_eq!(max_threads, 1);
|
||||
assert_eq!(*max_threads, 1);
|
||||
|
||||
registry.release_spawned_thread(thread_id);
|
||||
let reservation = registry
|
||||
@@ -147,10 +148,10 @@ fn release_is_idempotent_for_registered_threads() {
|
||||
Ok(_) => panic!("limit should still be enforced"),
|
||||
Err(err) => err,
|
||||
};
|
||||
let CodexErr::AgentLimitReached { max_threads } = err else {
|
||||
panic!("expected CodexErr::AgentLimitReached");
|
||||
let CodexErrorDetails::AgentLimitReached { max_threads } = err.details() else {
|
||||
panic!("expected AgentLimitReached");
|
||||
};
|
||||
assert_eq!(max_threads, 1);
|
||||
assert_eq!(*max_threads, 1);
|
||||
|
||||
registry.release_spawned_thread(second_id);
|
||||
let reservation = registry
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::mcp_tool_call::MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX;
|
||||
use async_channel::bounded;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::models::NetworkPermissions;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::AgentStatus;
|
||||
@@ -216,7 +217,10 @@ async fn run_codex_thread_interactive_respects_pre_cancelled_spawn() {
|
||||
.await
|
||||
.expect("cancelled delegate spawn should not hang");
|
||||
|
||||
assert!(matches!(result, Err(CodexErr::TurnAborted)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(err) if matches!(err.details(), CodexErrorDetails::TurnAborted)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -29,6 +29,7 @@ use codex_analytics::CompactionStrategy;
|
||||
use codex_analytics::CompactionTrigger;
|
||||
use codex_analytics::now_unix_seconds;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::items::ContextCompactionItem;
|
||||
use codex_protocol::items::TurnItem;
|
||||
@@ -291,16 +292,21 @@ async fn run_compact_task_inner_impl(
|
||||
Ok(()) => {
|
||||
break;
|
||||
}
|
||||
Err(err @ (CodexErr::Interrupted | CodexErr::TurnAborted)) => {
|
||||
Err(err)
|
||||
if matches!(
|
||||
err.details(),
|
||||
CodexErrorDetails::Interrupted | CodexErrorDetails::TurnAborted
|
||||
) =>
|
||||
{
|
||||
return Err(err);
|
||||
}
|
||||
Err(e @ CodexErr::SessionBudgetExceeded) => {
|
||||
Err(e) if matches!(e.details(), CodexErrorDetails::SessionBudgetExceeded) => {
|
||||
sess.track_turn_codex_error(turn_context.as_ref(), &e);
|
||||
let event = EventMsg::Error(e.to_error_event(/*message_prefix*/ None));
|
||||
sess.send_event(&turn_context, event).await;
|
||||
return Err(e);
|
||||
}
|
||||
Err(e @ CodexErr::ContextWindowExceeded) => {
|
||||
Err(e) if matches!(e.details(), CodexErrorDetails::ContextWindowExceeded) => {
|
||||
if turn_input_len > 1 {
|
||||
// Trim from the beginning to preserve cache (prefix-based) and keep recent messages intact.
|
||||
error!(
|
||||
@@ -479,7 +485,14 @@ impl CompactionAnalyticsAttempt {
|
||||
pub(crate) fn compaction_status_from_result<T>(result: &CodexResult<T>) -> CompactionStatus {
|
||||
match result {
|
||||
Ok(_) => CompactionStatus::Completed,
|
||||
Err(CodexErr::Interrupted | CodexErr::TurnAborted) => CompactionStatus::Interrupted,
|
||||
Err(err)
|
||||
if matches!(
|
||||
err.details(),
|
||||
CodexErrorDetails::Interrupted | CodexErrorDetails::TurnAborted
|
||||
) =>
|
||||
{
|
||||
CompactionStatus::Interrupted
|
||||
}
|
||||
Err(_) => CompactionStatus::Failed,
|
||||
}
|
||||
}
|
||||
@@ -697,7 +710,6 @@ async fn drain_to_completed(
|
||||
let Some(event) = maybe_event else {
|
||||
return Err(CodexErr::Stream(
|
||||
"stream closed before response.completed".into(),
|
||||
None,
|
||||
));
|
||||
};
|
||||
match event {
|
||||
|
||||
@@ -2,19 +2,20 @@ use codex_analytics::CompactionImplementation;
|
||||
use codex_analytics::CompactionReason;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use tracing::warn;
|
||||
|
||||
/// Retries failures that may be model-specific and succeed with a different model.
|
||||
pub(crate) fn should_retry_with_current_model(error: &CodexErr) -> bool {
|
||||
matches!(
|
||||
error,
|
||||
CodexErr::InvalidRequest(_)
|
||||
| CodexErr::UnexpectedStatus(_)
|
||||
| CodexErr::ContextWindowExceeded
|
||||
| CodexErr::UsageLimitReached(_)
|
||||
| CodexErr::ServerOverloaded
|
||||
| CodexErr::InternalServerError
|
||||
| CodexErr::RetryLimit(_)
|
||||
error.details(),
|
||||
CodexErrorDetails::InvalidRequest(_)
|
||||
| CodexErrorDetails::UnexpectedStatus(_)
|
||||
| CodexErrorDetails::ContextWindowExceeded
|
||||
| CodexErrorDetails::UsageLimitReached(_)
|
||||
| CodexErrorDetails::ServerOverloaded
|
||||
| CodexErrorDetails::InternalServerError
|
||||
| CodexErrorDetails::RetryLimit(_)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ use codex_analytics::CompactionPhase;
|
||||
use codex_analytics::CompactionReason;
|
||||
use codex_analytics::CompactionTrigger;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::items::ContextCompactionItem;
|
||||
use codex_protocol::items::TurnItem;
|
||||
@@ -185,7 +186,7 @@ async fn run_remote_compact_task_inner(
|
||||
.await;
|
||||
match result {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err @ CodexErr::TurnAborted) => Err(err),
|
||||
Err(err) if matches!(err.details(), CodexErrorDetails::TurnAborted) => Err(err),
|
||||
Err(err) => {
|
||||
sess.track_turn_codex_error(turn_context, &err);
|
||||
let event = EventMsg::Error(
|
||||
@@ -413,7 +414,6 @@ async fn collect_compaction_output(
|
||||
if !saw_completed {
|
||||
return Err(CodexErr::Stream(
|
||||
"remote compaction v2 stream closed before response.completed".to_string(),
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -48,12 +48,7 @@ pub(crate) async fn handle_retryable_response_stream_error(
|
||||
if *retries < max_retries {
|
||||
*retries += 1;
|
||||
let retry_count = *retries;
|
||||
let delay = match &err {
|
||||
CodexErr::Stream(_, requested_delay) => {
|
||||
requested_delay.unwrap_or_else(|| backoff(retry_count))
|
||||
}
|
||||
_ => backoff(retry_count),
|
||||
};
|
||||
let delay = err.retry_delay().unwrap_or_else(|| backoff(retry_count));
|
||||
log_retry(request, turn_context, &err, retry_count, max_retries, delay);
|
||||
|
||||
// In release builds, hide the first websocket retry notification to reduce noisy
|
||||
|
||||
@@ -20,10 +20,7 @@ async fn sampling_retry_logs_stream_error_context() {
|
||||
log_retry(
|
||||
ResponsesStreamRequest::Sampling,
|
||||
&turn_context,
|
||||
&CodexErr::Stream(
|
||||
"websocket closed by server before response.completed".to_string(),
|
||||
None,
|
||||
),
|
||||
&CodexErr::Stream("websocket closed by server before response.completed".to_string()),
|
||||
/*retries*/ 2,
|
||||
/*max_retries*/ 5,
|
||||
Duration::from_secs(1),
|
||||
|
||||
@@ -199,6 +199,7 @@ use codex_config::types::McpServerConfig;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::exec_output::StreamOutput;
|
||||
@@ -819,7 +820,7 @@ impl SessionIo {
|
||||
let session_loop_termination = self.session_loop_termination.clone();
|
||||
match self.submit(Op::Shutdown).await {
|
||||
Ok(_) => {}
|
||||
Err(CodexErr::InternalAgentDied) => {}
|
||||
Err(err) if matches!(err.details(), CodexErrorDetails::InternalAgentDied) => {}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
session_loop_termination.await;
|
||||
|
||||
@@ -87,6 +87,7 @@ use codex_protocol::config_types::AutoCompactTokenLimitScope;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::items::PlanItem;
|
||||
use codex_protocol::items::TurnItem;
|
||||
@@ -168,7 +169,7 @@ pub(crate) async fn run_turn(
|
||||
)
|
||||
.await
|
||||
{
|
||||
if matches!(err, CodexErr::TurnAborted) {
|
||||
if matches!(err.details(), CodexErrorDetails::TurnAborted) {
|
||||
run_hooks_and_record_inputs(&sess, &turn_context, &input).await;
|
||||
return Err(err);
|
||||
}
|
||||
@@ -185,7 +186,7 @@ pub(crate) async fn run_turn(
|
||||
.await
|
||||
{
|
||||
Ok(step_context) => step_context,
|
||||
Err(err @ CodexErr::TurnAborted) => {
|
||||
Err(err) if matches!(err.details(), CodexErrorDetails::TurnAborted) => {
|
||||
run_hooks_and_record_inputs(&sess, &turn_context, &input).await;
|
||||
return Err(err);
|
||||
}
|
||||
@@ -405,7 +406,7 @@ pub(crate) async fn run_turn(
|
||||
)
|
||||
.await
|
||||
{
|
||||
if matches!(err, CodexErr::TurnAborted) {
|
||||
if matches!(err.details(), CodexErrorDetails::TurnAborted) {
|
||||
return Err(err);
|
||||
}
|
||||
let error = err.to_codex_protocol_error();
|
||||
@@ -473,10 +474,15 @@ pub(crate) async fn run_turn(
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Err(err @ CodexErr::TurnAborted) => {
|
||||
Err(err) if matches!(err.details(), CodexErrorDetails::TurnAborted) => {
|
||||
return Err(err);
|
||||
}
|
||||
Err(codex_error @ CodexErr::InvalidImageRequest()) => {
|
||||
Err(codex_error)
|
||||
if matches!(
|
||||
codex_error.details(),
|
||||
CodexErrorDetails::InvalidImageRequest()
|
||||
) =>
|
||||
{
|
||||
sess.track_turn_codex_error(turn_context.as_ref(), &codex_error);
|
||||
let error = CodexErrorInfo::BadRequest;
|
||||
sess.emit_turn_error_lifecycle(turn_context.as_ref(), error.clone())
|
||||
@@ -1222,18 +1228,20 @@ async fn run_sampling_request(
|
||||
Ok(output) => {
|
||||
return Ok((output, original_input.unwrap_or(prompt.input)));
|
||||
}
|
||||
Err(CodexErr::ContextWindowExceeded) => {
|
||||
sess.set_total_tokens_full(&turn_context).await;
|
||||
return Err(CodexErr::ContextWindowExceeded);
|
||||
}
|
||||
Err(CodexErr::UsageLimitReached(e)) => {
|
||||
let rate_limits = e.rate_limits.clone();
|
||||
if let Some(rate_limits) = rate_limits {
|
||||
sess.update_rate_limits(&turn_context, *rate_limits).await;
|
||||
Err(err) => match err.details() {
|
||||
CodexErrorDetails::ContextWindowExceeded => {
|
||||
sess.set_total_tokens_full(&turn_context).await;
|
||||
return Err(err);
|
||||
}
|
||||
return Err(CodexErr::UsageLimitReached(e));
|
||||
}
|
||||
Err(err) => err,
|
||||
CodexErrorDetails::UsageLimitReached(e) => {
|
||||
let rate_limits = e.rate_limits.clone();
|
||||
if let Some(rate_limits) = rate_limits {
|
||||
sess.update_rate_limits(&turn_context, *rate_limits).await;
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
_ => err,
|
||||
},
|
||||
};
|
||||
|
||||
if original_input.is_none() {
|
||||
@@ -2074,7 +2082,9 @@ async fn try_run_sampling_request(
|
||||
.await
|
||||
{
|
||||
Ok(event) => event,
|
||||
Err(codex_async_utils::CancelErr::Cancelled) => break Err(CodexErr::TurnAborted),
|
||||
Err(codex_async_utils::CancelErr::Cancelled) => {
|
||||
break Err(CodexErr::TurnAborted);
|
||||
}
|
||||
};
|
||||
|
||||
let event = match event {
|
||||
@@ -2083,7 +2093,6 @@ async fn try_run_sampling_request(
|
||||
None => {
|
||||
break Err(CodexErr::Stream(
|
||||
"stream closed before response.completed".into(),
|
||||
None,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::session::TurnInput;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::state::TaskKind;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
@@ -76,7 +76,9 @@ impl SessionTask for CompactTask {
|
||||
}];
|
||||
crate::compact::run_compact_task(session.clone(), ctx, input).await
|
||||
};
|
||||
if let Err(err @ CodexErr::TurnAborted) = result {
|
||||
if let Err(err) = result
|
||||
&& matches!(err.details(), CodexErrorDetails::TurnAborted)
|
||||
{
|
||||
return Err(err);
|
||||
}
|
||||
Ok(None)
|
||||
|
||||
@@ -54,7 +54,7 @@ use codex_protocol::protocol::TurnCompleteEvent;
|
||||
use codex_protocol::protocol::WarningEvent;
|
||||
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::models::ContentItem;
|
||||
pub(crate) use compact::CompactTask;
|
||||
@@ -584,7 +584,9 @@ impl Session {
|
||||
) {
|
||||
let (last_agent_message, abort_reason) = match task_result {
|
||||
Ok(last_agent_message) => (last_agent_message, None),
|
||||
Err(CodexErr::TurnAborted) => (None, Some(TurnAbortReason::Interrupted)),
|
||||
Err(err) if matches!(err.details(), CodexErrorDetails::TurnAborted) => {
|
||||
(None, Some(TurnAbortReason::Interrupted))
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(%err, "session task returned an unexpected error");
|
||||
(None, None)
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::session::tests::build_world_state_from_turn_context;
|
||||
use crate::session::tests::make_session_and_context;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ReasoningItemReasoningSummary;
|
||||
use codex_protocol::protocol::InterAgentCommunication;
|
||||
@@ -169,8 +170,8 @@ fn truncate_rollout_after_turn_id_rejects_rolled_back_turn() {
|
||||
.expect_err("rolled-back turn should not be a fork anchor");
|
||||
|
||||
assert!(matches!(
|
||||
err,
|
||||
CodexErr::InvalidRequest(message)
|
||||
err.details(),
|
||||
CodexErrorDetails::InvalidRequest(message)
|
||||
if message == "lastTurnId 'turn-2' was not found in the source thread"
|
||||
));
|
||||
}
|
||||
@@ -188,8 +189,8 @@ fn truncate_rollout_after_turn_id_rejects_synthetic_legacy_turn_id() {
|
||||
.expect_err("synthetic turn should not be a fork anchor");
|
||||
|
||||
assert!(matches!(
|
||||
err,
|
||||
CodexErr::InvalidRequest(message)
|
||||
err.details(),
|
||||
CodexErrorDetails::InvalidRequest(message)
|
||||
if message
|
||||
== "lastTurnId 'rollout-0' is not a persisted canonical turn in the source thread"
|
||||
));
|
||||
@@ -203,8 +204,8 @@ fn truncate_rollout_after_turn_id_rejects_in_progress_turn() {
|
||||
.expect_err("in-progress turn should not be a fork anchor");
|
||||
|
||||
assert!(matches!(
|
||||
err,
|
||||
CodexErr::InvalidRequest(message)
|
||||
err.details(),
|
||||
CodexErrorDetails::InvalidRequest(message)
|
||||
if message == "lastTurnId 'turn-1' identifies an in-progress turn"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::session::turn_context::TurnContext;
|
||||
use crate::tools::context::SharedTurnDiffTracker;
|
||||
use crate::tools::sandboxing::ToolError;
|
||||
use codex_apply_patch::AppliedPatchDelta;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::error::SandboxErr;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_protocol::items::CommandExecutionItem;
|
||||
@@ -382,33 +382,37 @@ impl ToolEmitter {
|
||||
};
|
||||
(event, result)
|
||||
}
|
||||
Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Timeout { output }))) => {
|
||||
let response = self.format_exec_output_for_model(&output, ctx);
|
||||
let event = ToolEventStage::Failure(ToolEventFailure::Output(*output));
|
||||
let result = Err(FunctionCallError::RespondToModel(response));
|
||||
(event, result)
|
||||
}
|
||||
Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied { output, .. }))) => {
|
||||
let response = self.format_exec_output_for_model(&output, ctx);
|
||||
// apply_patch can be denied after it has already committed a
|
||||
// known prefix. Reuse the output-bearing path so the visible
|
||||
// item still fails while the turn diff consumes that prefix.
|
||||
let event = match (self, applied_patch_delta) {
|
||||
(Self::ApplyPatch { .. }, Some(delta)) => ToolEventStage::Success {
|
||||
output: *output,
|
||||
applied_patch_delta: Some(delta),
|
||||
},
|
||||
_ => ToolEventStage::Failure(ToolEventFailure::Output(*output)),
|
||||
};
|
||||
let result = Err(FunctionCallError::RespondToModel(response));
|
||||
(event, result)
|
||||
}
|
||||
Err(ToolError::Codex(err)) => {
|
||||
let message = format!("execution error: {err:?}");
|
||||
let event = ToolEventStage::Failure(ToolEventFailure::Message(message.clone()));
|
||||
let result = Err(FunctionCallError::RespondToModel(message));
|
||||
(event, result)
|
||||
}
|
||||
Err(ToolError::Codex(err)) => match err.details() {
|
||||
CodexErrorDetails::Sandbox(SandboxErr::Timeout { output }) => {
|
||||
let output = output.as_ref().clone();
|
||||
let response = self.format_exec_output_for_model(&output, ctx);
|
||||
let event = ToolEventStage::Failure(ToolEventFailure::Output(output));
|
||||
let result = Err(FunctionCallError::RespondToModel(response));
|
||||
(event, result)
|
||||
}
|
||||
CodexErrorDetails::Sandbox(SandboxErr::Denied { output, .. }) => {
|
||||
let output = output.as_ref().clone();
|
||||
let response = self.format_exec_output_for_model(&output, ctx);
|
||||
// apply_patch can be denied after it has already committed a
|
||||
// known prefix. Reuse the output-bearing path so the visible
|
||||
// item still fails while the turn diff consumes that prefix.
|
||||
let event = match (self, applied_patch_delta) {
|
||||
(Self::ApplyPatch { .. }, Some(delta)) => ToolEventStage::Success {
|
||||
output,
|
||||
applied_patch_delta: Some(delta),
|
||||
},
|
||||
_ => ToolEventStage::Failure(ToolEventFailure::Output(output)),
|
||||
};
|
||||
let result = Err(FunctionCallError::RespondToModel(response));
|
||||
(event, result)
|
||||
}
|
||||
_ => {
|
||||
let message = format!("execution error: {err:?}");
|
||||
let event = ToolEventStage::Failure(ToolEventFailure::Message(message.clone()));
|
||||
let result = Err(FunctionCallError::RespondToModel(message));
|
||||
(event, result)
|
||||
}
|
||||
},
|
||||
Err(ToolError::Rejected(msg)) => {
|
||||
// Normalize common rejection messages for exec tools so tests and
|
||||
// users see a clear, consistent phrase.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
use crate::tools::handlers::multi_agents_spec::create_close_agent_tool_v1;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
pub(crate) struct Handler;
|
||||
@@ -66,7 +66,9 @@ async fn handle_close_agent(
|
||||
.await
|
||||
{
|
||||
Ok(mut status_rx) => status_rx.borrow_and_update().clone(),
|
||||
Err(CodexErr::ThreadNotFound(_)) if known_agent => {
|
||||
Err(err)
|
||||
if known_agent && matches!(err.details(), CodexErrorDetails::ThreadNotFound(_)) =>
|
||||
{
|
||||
session.services.agent_control.get_status(agent_id).await
|
||||
}
|
||||
Err(err) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::agent::status::is_final;
|
||||
use crate::session::session::Session;
|
||||
use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions;
|
||||
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v1;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_tools::ToolSpec;
|
||||
use futures::FutureExt;
|
||||
use futures::StreamExt;
|
||||
@@ -125,7 +125,7 @@ impl Handler {
|
||||
}
|
||||
status_rxs.push((*id, rx));
|
||||
}
|
||||
Err(CodexErr::ThreadNotFound(_)) => {
|
||||
Err(err) if matches!(err.details(), CodexErrorDetails::ThreadNotFound(_)) => {
|
||||
initial_final_statuses.push((*id, AgentStatus::NotFound));
|
||||
}
|
||||
Err(err) => {
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_models_manager::manager::RefreshStrategy;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
@@ -80,27 +81,29 @@ where
|
||||
}
|
||||
|
||||
pub(crate) fn collab_spawn_error(err: CodexErr) -> FunctionCallError {
|
||||
match err {
|
||||
CodexErr::UnsupportedOperation(message) if message == "thread manager dropped" => {
|
||||
match err.details() {
|
||||
CodexErrorDetails::UnsupportedOperation(message) if message == "thread manager dropped" => {
|
||||
FunctionCallError::RespondToModel("collab manager unavailable".to_string())
|
||||
}
|
||||
CodexErr::UnsupportedOperation(message) => FunctionCallError::RespondToModel(message),
|
||||
err => FunctionCallError::RespondToModel(format!("collab spawn failed: {err}")),
|
||||
CodexErrorDetails::UnsupportedOperation(message) => {
|
||||
FunctionCallError::RespondToModel(message.clone())
|
||||
}
|
||||
_ => FunctionCallError::RespondToModel(format!("collab spawn failed: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn collab_agent_error(agent_id: ThreadId, err: CodexErr) -> FunctionCallError {
|
||||
match err {
|
||||
CodexErr::ThreadNotFound(id) => {
|
||||
match err.details() {
|
||||
CodexErrorDetails::ThreadNotFound(id) => {
|
||||
FunctionCallError::RespondToModel(format!("agent with id {id} not found"))
|
||||
}
|
||||
CodexErr::InternalAgentDied => {
|
||||
CodexErrorDetails::InternalAgentDied => {
|
||||
FunctionCallError::RespondToModel(format!("agent with id {agent_id} is closed"))
|
||||
}
|
||||
CodexErr::UnsupportedOperation(_) => {
|
||||
CodexErrorDetails::UnsupportedOperation(_) => {
|
||||
FunctionCallError::RespondToModel("collab manager unavailable".to_string())
|
||||
}
|
||||
err => FunctionCallError::RespondToModel(format!("collab tool failed: {err}")),
|
||||
_ => FunctionCallError::RespondToModel(format!("collab tool failed: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
use crate::tools::handlers::multi_agents_spec::create_interrupt_agent_tool_v2;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
pub(crate) struct Handler;
|
||||
@@ -66,7 +66,15 @@ async fn handle_interrupt_agent(
|
||||
.interrupt_agent(agent_id)
|
||||
.await
|
||||
{
|
||||
Ok(_) | Err(CodexErr::ThreadNotFound(_)) | Err(CodexErr::InternalAgentDied) => Ok(()),
|
||||
Ok(_) => Ok(()),
|
||||
Err(err)
|
||||
if matches!(
|
||||
err.details(),
|
||||
CodexErrorDetails::ThreadNotFound(_) | CodexErrorDetails::InternalAgentDied
|
||||
) =>
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(collab_agent_error(agent_id, err)),
|
||||
};
|
||||
result?;
|
||||
|
||||
@@ -27,7 +27,7 @@ use crate::tools::sandboxing::default_exec_approval_requirement;
|
||||
use crate::tools::sandboxing::sandbox_override_for_first_attempt;
|
||||
use crate::tools::sandboxing::unsandboxed_execution_allowed;
|
||||
use codex_otel::ToolDecisionSource;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::error::SandboxErr;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
@@ -298,10 +298,24 @@ impl ToolOrchestrator {
|
||||
deferred_network_approval: first_deferred_network_approval,
|
||||
})
|
||||
}
|
||||
Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {
|
||||
output,
|
||||
network_policy_decision,
|
||||
}))) => {
|
||||
Err(ToolError::Codex(err)) => {
|
||||
let CodexErrorDetails::Sandbox(SandboxErr::Denied {
|
||||
output,
|
||||
network_policy_decision,
|
||||
}) = err.details()
|
||||
else {
|
||||
let err = ToolError::Codex(err);
|
||||
if let Some(outcome) = sandbox_outcome_from_tool_error(&err) {
|
||||
otel.sandbox_outcome(
|
||||
&otel_tn,
|
||||
otel_ci,
|
||||
outcome,
|
||||
initial_duration,
|
||||
/*escalated_duration*/ None,
|
||||
);
|
||||
}
|
||||
return Err(err);
|
||||
};
|
||||
let network_approval_context = if managed_network_active {
|
||||
network_policy_decision
|
||||
.as_ref()
|
||||
@@ -317,10 +331,7 @@ impl ToolOrchestrator {
|
||||
initial_duration,
|
||||
/*escalated_duration*/ None,
|
||||
);
|
||||
return Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {
|
||||
output,
|
||||
network_policy_decision,
|
||||
})));
|
||||
return Err(ToolError::Codex(err));
|
||||
}
|
||||
if !tool.escalate_on_failure() {
|
||||
otel.sandbox_outcome(
|
||||
@@ -330,10 +341,7 @@ impl ToolOrchestrator {
|
||||
initial_duration,
|
||||
/*escalated_duration*/ None,
|
||||
);
|
||||
return Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {
|
||||
output,
|
||||
network_policy_decision,
|
||||
})));
|
||||
return Err(ToolError::Codex(err));
|
||||
}
|
||||
let unsandboxed_allowed =
|
||||
unsandboxed_execution_allowed(&file_system_sandbox_policy);
|
||||
@@ -359,10 +367,7 @@ impl ToolOrchestrator {
|
||||
initial_duration,
|
||||
/*escalated_duration*/ None,
|
||||
);
|
||||
return Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {
|
||||
output,
|
||||
network_policy_decision,
|
||||
})));
|
||||
return Err(ToolError::Codex(err));
|
||||
}
|
||||
}
|
||||
if !unsandboxed_allowed && network_approval_context.is_none() {
|
||||
@@ -373,10 +378,7 @@ impl ToolOrchestrator {
|
||||
initial_duration,
|
||||
/*escalated_duration*/ None,
|
||||
);
|
||||
return Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {
|
||||
output,
|
||||
network_policy_decision,
|
||||
})));
|
||||
return Err(ToolError::Codex(err));
|
||||
}
|
||||
let retry_reason =
|
||||
if let Some(network_approval_context) = network_approval_context.as_ref() {
|
||||
@@ -514,10 +516,13 @@ impl ToolOrchestrator {
|
||||
|
||||
fn sandbox_outcome_from_tool_error(err: &ToolError) -> Option<&'static str> {
|
||||
match err {
|
||||
ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied { .. })) => Some("denied"),
|
||||
ToolError::Codex(CodexErr::Sandbox(SandboxErr::Timeout { .. })) => Some("timed_out"),
|
||||
ToolError::Codex(CodexErr::Sandbox(SandboxErr::Signal(_))) => Some("signal"),
|
||||
ToolError::Rejected(_) | ToolError::Codex(_) => None,
|
||||
ToolError::Codex(err) => match err.details() {
|
||||
CodexErrorDetails::Sandbox(SandboxErr::Denied { .. }) => Some("denied"),
|
||||
CodexErrorDetails::Sandbox(SandboxErr::Timeout { .. }) => Some("timed_out"),
|
||||
CodexErrorDetails::Sandbox(SandboxErr::Signal(_)) => Some("signal"),
|
||||
_ => None,
|
||||
},
|
||||
ToolError::Rejected(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ use crate::unified_exec::process::UnifiedExecProcess;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::error::SandboxErr;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ExecCommandSource;
|
||||
@@ -1203,16 +1204,19 @@ impl UnifiedExecProcessManager {
|
||||
.await
|
||||
.map(|result| (result.output, result.deferred_network_approval))
|
||||
.map_err(|err| match err {
|
||||
ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied { output, .. })) => {
|
||||
let output = *output;
|
||||
let message = if output.aggregated_output.text.is_empty() {
|
||||
let exit_code = output.exit_code;
|
||||
format!("Process exited with code {exit_code}")
|
||||
} else {
|
||||
output.aggregated_output.text.clone()
|
||||
};
|
||||
UnifiedExecError::sandbox_denied(message, output)
|
||||
}
|
||||
ToolError::Codex(err) => match err.details() {
|
||||
CodexErrorDetails::Sandbox(SandboxErr::Denied { output, .. }) => {
|
||||
let output = output.as_ref().clone();
|
||||
let message = if output.aggregated_output.text.is_empty() {
|
||||
let exit_code = output.exit_code;
|
||||
format!("Process exited with code {exit_code}")
|
||||
} else {
|
||||
output.aggregated_output.text.clone()
|
||||
};
|
||||
UnifiedExecError::sandbox_denied(message, output)
|
||||
}
|
||||
_ => UnifiedExecError::create_process(format!("{err:?}")),
|
||||
},
|
||||
other => UnifiedExecError::create_process(format!("{other:?}")),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use codex_core::exec_env::create_env;
|
||||
use codex_core::sandboxing::SandboxPermissions;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::error::Result;
|
||||
use codex_protocol::error::SandboxErr;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
@@ -218,13 +218,15 @@ async fn should_skip_bwrap_tests() -> bool {
|
||||
.await
|
||||
{
|
||||
Ok(output) => is_bwrap_unavailable_output(&output),
|
||||
Err(CodexErr::Sandbox(SandboxErr::Denied { output, .. })) => {
|
||||
is_bwrap_unavailable_output(&output)
|
||||
}
|
||||
// Probe timeouts are not actionable for the bwrap-specific assertions below;
|
||||
// skip rather than fail the whole suite.
|
||||
Err(CodexErr::Sandbox(SandboxErr::Timeout { .. })) => true,
|
||||
Err(err) => panic!("bwrap availability probe failed unexpectedly: {err:?}"),
|
||||
Err(err) => match err.details() {
|
||||
CodexErrorDetails::Sandbox(SandboxErr::Denied { output, .. }) => {
|
||||
is_bwrap_unavailable_output(output)
|
||||
}
|
||||
// Probe timeouts are not actionable for the bwrap-specific assertions below;
|
||||
// skip rather than fail the whole suite.
|
||||
CodexErrorDetails::Sandbox(SandboxErr::Timeout { .. }) => true,
|
||||
details => panic!("bwrap availability probe failed unexpectedly: {details:?}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,8 +239,12 @@ fn expect_denied(
|
||||
assert_ne!(output.exit_code, 0, "{context}: expected nonzero exit code");
|
||||
output
|
||||
}
|
||||
Err(CodexErr::Sandbox(SandboxErr::Denied { output, .. })) => *output,
|
||||
Err(err) => panic!("{context}: {err:?}"),
|
||||
Err(err) => match err.details() {
|
||||
CodexErrorDetails::Sandbox(SandboxErr::Denied { output, .. }) => {
|
||||
output.as_ref().clone()
|
||||
}
|
||||
details => panic!("{context}: {details:?}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,10 +462,12 @@ async fn assert_network_blocked(cmd: &[&str]) {
|
||||
|
||||
let output = match result {
|
||||
Ok(output) => output,
|
||||
Err(CodexErr::Sandbox(SandboxErr::Denied { output, .. })) => *output,
|
||||
_ => {
|
||||
panic!("expected sandbox denied error, got: {result:?}");
|
||||
}
|
||||
Err(err) => match err.details() {
|
||||
CodexErrorDetails::Sandbox(SandboxErr::Denied { output, .. }) => {
|
||||
output.as_ref().clone()
|
||||
}
|
||||
details => panic!("expected sandbox denied error, got: {details:?}"),
|
||||
},
|
||||
};
|
||||
|
||||
dbg!(&output.stderr.text);
|
||||
@@ -612,8 +620,13 @@ async fn sandbox_reports_codex_symlink_build_failure_without_panicking() {
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(CodexErr::Sandbox(SandboxErr::Denied { output, .. })) => *output,
|
||||
result => panic!(".codex symlink build failure should deny: {result:?}"),
|
||||
Err(err) => match err.details() {
|
||||
CodexErrorDetails::Sandbox(SandboxErr::Denied { output, .. }) => {
|
||||
output.as_ref().clone()
|
||||
}
|
||||
details => panic!(".codex symlink build failure should deny: {details:?}"),
|
||||
},
|
||||
Ok(output) => panic!(".codex symlink build failure should deny: {output:?}"),
|
||||
};
|
||||
|
||||
assert_eq!(output.exit_code, 1);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use codex_api::ApiError;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use http::StatusCode;
|
||||
|
||||
pub(super) const BEDROCK_EXPIRED_SIGNATURE_MESSAGE: &str = concat!(
|
||||
@@ -9,12 +10,18 @@ pub(super) const BEDROCK_EXPIRED_SIGNATURE_MESSAGE: &str = concat!(
|
||||
);
|
||||
|
||||
pub(super) fn map_api_error(error: ApiError) -> CodexErr {
|
||||
let mut error = codex_api::map_api_error(error);
|
||||
if let CodexErr::UnexpectedStatus(response) = &mut error
|
||||
let error = codex_api::map_api_error(error);
|
||||
if let CodexErrorDetails::UnexpectedStatus(response) = error.details()
|
||||
&& response.status == StatusCode::UNAUTHORIZED
|
||||
&& response.body.contains("Signature expired:")
|
||||
{
|
||||
let mut response = response.clone();
|
||||
response.user_message = Some(BEDROCK_EXPIRED_SIGNATURE_MESSAGE.to_string());
|
||||
let mapped_error = CodexErr::new(CodexErrorDetails::UnexpectedStatus(response));
|
||||
return match error.retry_delay() {
|
||||
Some(retry_delay) => mapped_error.with_retry_delay(retry_delay),
|
||||
None => mapped_error,
|
||||
};
|
||||
}
|
||||
error
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use codex_api::ApiError;
|
||||
use codex_api::TransportError;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use http::HeaderMap;
|
||||
use http::HeaderValue;
|
||||
use http::StatusCode;
|
||||
@@ -29,7 +29,7 @@ fn expired_signature_has_actionable_guidance() {
|
||||
"Signature expired: 20260609T133205Z is now earlier than 20260614T062525Z",
|
||||
));
|
||||
|
||||
let CodexErr::UnexpectedStatus(response) = &error else {
|
||||
let CodexErrorDetails::UnexpectedStatus(response) = error.details() else {
|
||||
panic!("expected unexpected status error, got {error:?}");
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -51,7 +51,7 @@ fn other_unauthorized_errors_remain_generic() {
|
||||
"The security token included in the request is invalid",
|
||||
));
|
||||
|
||||
let CodexErr::UnexpectedStatus(response) = &error else {
|
||||
let CodexErrorDetails::UnexpectedStatus(response) = error.details() else {
|
||||
panic!("expected unexpected status error, got {error:?}");
|
||||
};
|
||||
assert_eq!(response.user_message, None);
|
||||
@@ -70,7 +70,7 @@ fn signature_errors_with_other_statuses_remain_generic() {
|
||||
"Signature expired: old is now earlier than new",
|
||||
));
|
||||
|
||||
let CodexErr::UnexpectedStatus(response) = &error else {
|
||||
let CodexErrorDetails::UnexpectedStatus(response) = error.details() else {
|
||||
panic!("expected unexpected status error, got {error:?}");
|
||||
};
|
||||
assert_eq!(response.user_message, None);
|
||||
|
||||
@@ -323,6 +323,7 @@ mod tests {
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_model_provider_info::create_oss_provider_with_base_url;
|
||||
use codex_protocol::account::PlanType;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use http::header::AUTHORIZATION;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
@@ -471,10 +472,12 @@ mod tests {
|
||||
});
|
||||
|
||||
match resolve_provider_auth(Some(&auth), &provider) {
|
||||
Err(CodexErr::UnsupportedOperation(message)) => {
|
||||
assert_eq!(message, BEDROCK_API_KEY_UNSUPPORTED_MESSAGE);
|
||||
}
|
||||
Err(err) => panic!("unexpected auth error: {err:?}"),
|
||||
Err(err) => match err.details() {
|
||||
CodexErrorDetails::UnsupportedOperation(message) => {
|
||||
assert_eq!(message, BEDROCK_API_KEY_UNSUPPORTED_MESSAGE);
|
||||
}
|
||||
details => panic!("unexpected auth error: {details:?}"),
|
||||
},
|
||||
Ok(_) => panic!("Bedrock API key auth should be rejected"),
|
||||
}
|
||||
}
|
||||
|
||||
+270
-77
@@ -19,8 +19,10 @@ use codex_utils_string::truncate_middle_chars;
|
||||
use codex_utils_string::truncate_middle_with_token_budget;
|
||||
use reqwest::StatusCode;
|
||||
use serde_json;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::time::Duration;
|
||||
use strum_macros::EnumDiscriminants;
|
||||
use thiserror::Error;
|
||||
use tokio::task::JoinError;
|
||||
|
||||
@@ -64,8 +66,18 @@ pub enum SandboxErr {
|
||||
LandlockRestrict,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CodexErr {
|
||||
pub struct CodexErr {
|
||||
details: CodexErrorDetails,
|
||||
retry_delay: Option<Duration>,
|
||||
}
|
||||
|
||||
/// The semantic category and diagnostic payload for a [`CodexErr`].
|
||||
#[derive(Error, Debug, EnumDiscriminants)]
|
||||
#[strum_discriminants(name(CodexErrKind))]
|
||||
#[strum_discriminants(derive(serde::Serialize))]
|
||||
#[strum_discriminants(serde(rename_all = "snake_case"))]
|
||||
#[strum_discriminants(doc = "The payload-free semantic category used for analytics.")]
|
||||
pub enum CodexErrorDetails {
|
||||
#[error("turn aborted. Something went wrong? Hit `/feedback` to report the issue.")]
|
||||
TurnAborted,
|
||||
|
||||
@@ -76,10 +88,8 @@ pub enum CodexErr {
|
||||
/// handshake has succeeded but **before** it finished emitting `response.completed`.
|
||||
///
|
||||
/// The Session loop treats this as a transient error and will automatically retry the turn.
|
||||
///
|
||||
/// Optionally includes the requested delay before retrying the turn.
|
||||
#[error("stream disconnected before completion: {0}")]
|
||||
Stream(String, Option<Duration>),
|
||||
Stream(String),
|
||||
#[error(
|
||||
"Codex ran out of room in the model's context window. Start a new thread or clear earlier history before retrying."
|
||||
)]
|
||||
@@ -166,87 +176,270 @@ pub enum CodexErr {
|
||||
EnvVar(EnvVarError),
|
||||
}
|
||||
|
||||
impl From<CancelErr> for CodexErr {
|
||||
fn from(_: CancelErr) -> Self {
|
||||
CodexErr::TurnAborted
|
||||
impl From<&CodexErr> for CodexErrKind {
|
||||
fn from(error: &CodexErr) -> Self {
|
||||
error.details().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl CodexErr {
|
||||
pub fn is_retryable(&self) -> bool {
|
||||
match self {
|
||||
CodexErr::TurnAborted
|
||||
| CodexErr::SessionBudgetExceeded
|
||||
| CodexErr::Interrupted
|
||||
| CodexErr::EnvVar(_)
|
||||
| CodexErr::Fatal(_)
|
||||
| CodexErr::UsageNotIncluded
|
||||
| CodexErr::QuotaExceeded
|
||||
| CodexErr::InvalidImageRequest()
|
||||
| CodexErr::InvalidRequest(_)
|
||||
| CodexErr::RefreshTokenFailed(_)
|
||||
| CodexErr::UnsupportedOperation(_)
|
||||
| CodexErr::Sandbox(_)
|
||||
| CodexErr::LandlockSandboxExecutableNotProvided
|
||||
| CodexErr::RetryLimit(_)
|
||||
| CodexErr::ContextWindowExceeded
|
||||
| CodexErr::ThreadNotFound(_)
|
||||
| CodexErr::AgentLimitReached { .. }
|
||||
| CodexErr::Spawn
|
||||
| CodexErr::SessionConfiguredNotFirstEvent
|
||||
| CodexErr::UsageLimitReached(_)
|
||||
| CodexErr::ServerOverloaded
|
||||
| CodexErr::CyberPolicy { .. } => false,
|
||||
CodexErr::Stream(..)
|
||||
| CodexErr::Timeout
|
||||
| CodexErr::RequestTimeout
|
||||
| CodexErr::UnexpectedStatus(_)
|
||||
| CodexErr::ResponseStreamFailed(_)
|
||||
| CodexErr::ConnectionFailed(_)
|
||||
| CodexErr::InternalServerError
|
||||
| CodexErr::InternalAgentDied
|
||||
| CodexErr::Io(_)
|
||||
| CodexErr::Json(_)
|
||||
| CodexErr::TokioJoin(_) => true,
|
||||
#[cfg(target_os = "linux")]
|
||||
CodexErr::LandlockRuleset(_) | CodexErr::LandlockPathFd(_) => false,
|
||||
impl fmt::Debug for CodexErr {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self.details {
|
||||
CodexErrorDetails::Stream(message) => formatter
|
||||
.debug_tuple("Stream")
|
||||
.field(message)
|
||||
.field(&self.retry_delay)
|
||||
.finish(),
|
||||
details => fmt::Debug::fmt(details, formatter),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CodexErr {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Display::fmt(&self.details, formatter)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CodexErr {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
self.details.source()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CodexErrorDetails> for CodexErr {
|
||||
fn from(details: CodexErrorDetails) -> Self {
|
||||
Self {
|
||||
details,
|
||||
retry_delay: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CancelErr> for CodexErr {
|
||||
fn from(error: CancelErr) -> Self {
|
||||
CodexErrorDetails::from(error).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SandboxErr> for CodexErr {
|
||||
fn from(error: SandboxErr) -> Self {
|
||||
CodexErrorDetails::from(error).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for CodexErr {
|
||||
fn from(error: io::Error) -> Self {
|
||||
CodexErrorDetails::from(error).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for CodexErr {
|
||||
fn from(error: serde_json::Error) -> Self {
|
||||
CodexErrorDetails::from(error).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JoinError> for CodexErr {
|
||||
fn from(error: JoinError) -> Self {
|
||||
CodexErrorDetails::from(error).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl From<landlock::RulesetError> for CodexErr {
|
||||
fn from(error: landlock::RulesetError) -> Self {
|
||||
CodexErrorDetails::from(error).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl From<landlock::PathFdError> for CodexErr {
|
||||
fn from(error: landlock::PathFdError) -> Self {
|
||||
CodexErrorDetails::from(error).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CancelErr> for CodexErrorDetails {
|
||||
fn from(_: CancelErr) -> Self {
|
||||
CodexErrorDetails::TurnAborted
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(anp): Remove this compatibility macro once callers construct
|
||||
// `CodexErrorDetails` directly.
|
||||
macro_rules! codex_err_unit_constructors {
|
||||
($($variant:ident),* $(,)?) => {
|
||||
$(
|
||||
#[doc(hidden)]
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const $variant: Self = Self {
|
||||
details: CodexErrorDetails::$variant,
|
||||
retry_delay: None,
|
||||
};
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(anp): Remove this compatibility macro once callers construct
|
||||
// `CodexErrorDetails` directly.
|
||||
macro_rules! codex_err_tuple_constructors {
|
||||
($($(#[$attr:meta])* $variant:ident($value:ident: $value_type:ty)),* $(,)?) => {
|
||||
$(
|
||||
$(#[$attr])*
|
||||
#[doc(hidden)]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn $variant($value: $value_type) -> Self {
|
||||
CodexErrorDetails::$variant($value).into()
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
impl CodexErr {
|
||||
codex_err_unit_constructors!(
|
||||
TurnAborted,
|
||||
SessionBudgetExceeded,
|
||||
ContextWindowExceeded,
|
||||
SessionConfiguredNotFirstEvent,
|
||||
Timeout,
|
||||
RequestTimeout,
|
||||
Spawn,
|
||||
Interrupted,
|
||||
ServerOverloaded,
|
||||
QuotaExceeded,
|
||||
UsageNotIncluded,
|
||||
InternalServerError,
|
||||
InternalAgentDied,
|
||||
LandlockSandboxExecutableNotProvided,
|
||||
);
|
||||
|
||||
codex_err_tuple_constructors!(
|
||||
Stream(message: String),
|
||||
ThreadNotFound(thread_id: ThreadId),
|
||||
UnexpectedStatus(error: UnexpectedResponseError),
|
||||
InvalidRequest(message: String),
|
||||
UsageLimitReached(error: UsageLimitReachedError),
|
||||
ResponseStreamFailed(error: ResponseStreamFailed),
|
||||
ConnectionFailed(error: ConnectionFailedError),
|
||||
RetryLimit(error: RetryLimitReachedError),
|
||||
Sandbox(error: SandboxErr),
|
||||
UnsupportedOperation(message: String),
|
||||
RefreshTokenFailed(error: RefreshTokenFailedError),
|
||||
Fatal(message: String),
|
||||
Io(error: io::Error),
|
||||
Json(error: serde_json::Error),
|
||||
#[cfg(target_os = "linux")]
|
||||
LandlockRuleset(error: landlock::RulesetError),
|
||||
#[cfg(target_os = "linux")]
|
||||
LandlockPathFd(error: landlock::PathFdError),
|
||||
TokioJoin(error: JoinError),
|
||||
EnvVar(error: EnvVarError),
|
||||
);
|
||||
|
||||
// TODO(anp): Remove this compatibility constructor once callers construct
|
||||
// `CodexErrorDetails` directly.
|
||||
#[doc(hidden)]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn InvalidImageRequest() -> Self {
|
||||
CodexErrorDetails::InvalidImageRequest().into()
|
||||
}
|
||||
|
||||
/// Creates an error with no server-provided retry delay.
|
||||
pub fn new(details: CodexErrorDetails) -> Self {
|
||||
details.into()
|
||||
}
|
||||
|
||||
/// Returns the semantic failure and its diagnostic payload.
|
||||
pub fn details(&self) -> &CodexErrorDetails {
|
||||
&self.details
|
||||
}
|
||||
|
||||
pub fn is_retryable(&self) -> bool {
|
||||
match self.details() {
|
||||
CodexErrorDetails::TurnAborted
|
||||
| CodexErrorDetails::SessionBudgetExceeded
|
||||
| CodexErrorDetails::Interrupted
|
||||
| CodexErrorDetails::EnvVar(_)
|
||||
| CodexErrorDetails::Fatal(_)
|
||||
| CodexErrorDetails::UsageNotIncluded
|
||||
| CodexErrorDetails::QuotaExceeded
|
||||
| CodexErrorDetails::InvalidImageRequest()
|
||||
| CodexErrorDetails::InvalidRequest(_)
|
||||
| CodexErrorDetails::RefreshTokenFailed(_)
|
||||
| CodexErrorDetails::UnsupportedOperation(_)
|
||||
| CodexErrorDetails::Sandbox(_)
|
||||
| CodexErrorDetails::LandlockSandboxExecutableNotProvided
|
||||
| CodexErrorDetails::RetryLimit(_)
|
||||
| CodexErrorDetails::ContextWindowExceeded
|
||||
| CodexErrorDetails::ThreadNotFound(_)
|
||||
| CodexErrorDetails::AgentLimitReached { .. }
|
||||
| CodexErrorDetails::Spawn
|
||||
| CodexErrorDetails::SessionConfiguredNotFirstEvent
|
||||
| CodexErrorDetails::UsageLimitReached(_)
|
||||
| CodexErrorDetails::ServerOverloaded
|
||||
| CodexErrorDetails::CyberPolicy { .. } => false,
|
||||
CodexErrorDetails::Stream(..)
|
||||
| CodexErrorDetails::Timeout
|
||||
| CodexErrorDetails::RequestTimeout
|
||||
| CodexErrorDetails::UnexpectedStatus(_)
|
||||
| CodexErrorDetails::ResponseStreamFailed(_)
|
||||
| CodexErrorDetails::ConnectionFailed(_)
|
||||
| CodexErrorDetails::InternalServerError
|
||||
| CodexErrorDetails::InternalAgentDied
|
||||
| CodexErrorDetails::Io(_)
|
||||
| CodexErrorDetails::Json(_)
|
||||
| CodexErrorDetails::TokioJoin(_) => true,
|
||||
#[cfg(target_os = "linux")]
|
||||
CodexErrorDetails::LandlockRuleset(_) | CodexErrorDetails::LandlockPathFd(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn retry_delay(&self) -> Option<Duration> {
|
||||
self.retry_delay
|
||||
}
|
||||
|
||||
pub fn with_retry_delay(mut self, retry_delay: Duration) -> Self {
|
||||
self.retry_delay = Some(retry_delay);
|
||||
self
|
||||
}
|
||||
|
||||
/// Minimal shim so that existing `e.downcast_ref::<CodexErr>()` checks continue to compile
|
||||
/// after replacing `anyhow::Error` in the return signature. This mirrors the behavior of
|
||||
/// `anyhow::Error::downcast_ref` but works directly on our concrete enum.
|
||||
/// `anyhow::Error::downcast_ref` but works directly on our concrete error type.
|
||||
pub fn downcast_ref<T: std::any::Any>(&self) -> Option<&T> {
|
||||
(self as &dyn std::any::Any).downcast_ref::<T>()
|
||||
}
|
||||
|
||||
/// Translate core error to client-facing protocol error.
|
||||
pub fn to_codex_protocol_error(&self) -> CodexErrorInfo {
|
||||
match self {
|
||||
CodexErr::ContextWindowExceeded => CodexErrorInfo::ContextWindowExceeded,
|
||||
CodexErr::SessionBudgetExceeded => CodexErrorInfo::SessionBudgetExceeded,
|
||||
CodexErr::UsageLimitReached(_)
|
||||
| CodexErr::QuotaExceeded
|
||||
| CodexErr::UsageNotIncluded => CodexErrorInfo::UsageLimitExceeded,
|
||||
CodexErr::ServerOverloaded => CodexErrorInfo::ServerOverloaded,
|
||||
CodexErr::CyberPolicy { .. } => CodexErrorInfo::CyberPolicy,
|
||||
CodexErr::RetryLimit(_) => CodexErrorInfo::ResponseTooManyFailedAttempts {
|
||||
match &self.details {
|
||||
CodexErrorDetails::ContextWindowExceeded => CodexErrorInfo::ContextWindowExceeded,
|
||||
CodexErrorDetails::SessionBudgetExceeded => CodexErrorInfo::SessionBudgetExceeded,
|
||||
CodexErrorDetails::UsageLimitReached(_)
|
||||
| CodexErrorDetails::QuotaExceeded
|
||||
| CodexErrorDetails::UsageNotIncluded => CodexErrorInfo::UsageLimitExceeded,
|
||||
CodexErrorDetails::ServerOverloaded => CodexErrorInfo::ServerOverloaded,
|
||||
CodexErrorDetails::CyberPolicy { .. } => CodexErrorInfo::CyberPolicy,
|
||||
CodexErrorDetails::RetryLimit(_) => CodexErrorInfo::ResponseTooManyFailedAttempts {
|
||||
http_status_code: self.http_status_code_value(),
|
||||
},
|
||||
CodexErr::ConnectionFailed(_) => CodexErrorInfo::HttpConnectionFailed {
|
||||
CodexErrorDetails::ConnectionFailed(_) => CodexErrorInfo::HttpConnectionFailed {
|
||||
http_status_code: self.http_status_code_value(),
|
||||
},
|
||||
CodexErr::ResponseStreamFailed(_) => CodexErrorInfo::ResponseStreamConnectionFailed {
|
||||
http_status_code: self.http_status_code_value(),
|
||||
},
|
||||
CodexErr::RefreshTokenFailed(_) => CodexErrorInfo::Unauthorized,
|
||||
CodexErr::SessionConfiguredNotFirstEvent
|
||||
| CodexErr::InternalServerError
|
||||
| CodexErr::InternalAgentDied => CodexErrorInfo::InternalServerError,
|
||||
CodexErr::UnsupportedOperation(_)
|
||||
| CodexErr::ThreadNotFound(_)
|
||||
| CodexErr::AgentLimitReached { .. } => CodexErrorInfo::BadRequest,
|
||||
CodexErr::Sandbox(_) => CodexErrorInfo::SandboxError,
|
||||
CodexErrorDetails::ResponseStreamFailed(_) => {
|
||||
CodexErrorInfo::ResponseStreamConnectionFailed {
|
||||
http_status_code: self.http_status_code_value(),
|
||||
}
|
||||
}
|
||||
CodexErrorDetails::RefreshTokenFailed(_) => CodexErrorInfo::Unauthorized,
|
||||
CodexErrorDetails::SessionConfiguredNotFirstEvent
|
||||
| CodexErrorDetails::InternalServerError
|
||||
| CodexErrorDetails::InternalAgentDied => CodexErrorInfo::InternalServerError,
|
||||
CodexErrorDetails::UnsupportedOperation(_)
|
||||
| CodexErrorDetails::ThreadNotFound(_)
|
||||
| CodexErrorDetails::AgentLimitReached { .. } => CodexErrorInfo::BadRequest,
|
||||
CodexErrorDetails::Sandbox(_) => CodexErrorInfo::SandboxError,
|
||||
_ => CodexErrorInfo::Other,
|
||||
}
|
||||
}
|
||||
@@ -264,11 +457,11 @@ impl CodexErr {
|
||||
}
|
||||
|
||||
pub fn http_status_code_value(&self) -> Option<u16> {
|
||||
let http_status_code = match self {
|
||||
CodexErr::RetryLimit(err) => Some(err.status),
|
||||
CodexErr::UnexpectedStatus(err) => Some(err.status),
|
||||
CodexErr::ConnectionFailed(err) => err.source.status(),
|
||||
CodexErr::ResponseStreamFailed(err) => err.source.status(),
|
||||
let http_status_code = match &self.details {
|
||||
CodexErrorDetails::RetryLimit(err) => Some(err.status),
|
||||
CodexErrorDetails::UnexpectedStatus(err) => Some(err.status),
|
||||
CodexErrorDetails::ConnectionFailed(err) => err.source.status(),
|
||||
CodexErrorDetails::ResponseStreamFailed(err) => err.source.status(),
|
||||
_ => None,
|
||||
};
|
||||
http_status_code.as_ref().map(StatusCode::as_u16)
|
||||
@@ -306,7 +499,7 @@ impl std::fmt::Display for ResponseStreamFailed {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UnexpectedResponseError {
|
||||
pub status: StatusCode,
|
||||
pub body: String,
|
||||
@@ -606,8 +799,8 @@ impl std::fmt::Display for EnvVarError {
|
||||
}
|
||||
|
||||
pub fn get_error_message_ui(e: &CodexErr) -> String {
|
||||
let message = match e {
|
||||
CodexErr::Sandbox(SandboxErr::Denied { output, .. }) => {
|
||||
let message = match e.details() {
|
||||
CodexErrorDetails::Sandbox(SandboxErr::Denied { output, .. }) => {
|
||||
let aggregated = output.aggregated_output.text.trim();
|
||||
if !aggregated.is_empty() {
|
||||
output.aggregated_output.text.clone()
|
||||
@@ -626,7 +819,7 @@ pub fn get_error_message_ui(e: &CodexErr) -> String {
|
||||
}
|
||||
}
|
||||
// Timeouts are not sandbox errors from a UX perspective; present them plainly.
|
||||
CodexErr::Sandbox(SandboxErr::Timeout { output }) => {
|
||||
CodexErrorDetails::Sandbox(SandboxErr::Timeout { output }) => {
|
||||
format!(
|
||||
"error: command timed out after {} ms",
|
||||
output.duration.as_millis()
|
||||
|
||||
@@ -11,6 +11,64 @@ use reqwest::Response;
|
||||
use reqwest::ResponseBuilderExt;
|
||||
use reqwest::StatusCode;
|
||||
use reqwest::Url;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn codex_err_debug_preserves_legacy_shape() {
|
||||
let actual = [
|
||||
CodexErr::Timeout,
|
||||
CodexErr::Stream("disconnected".to_string()),
|
||||
CodexErr::Stream("retry later".to_string()).with_retry_delay(Duration::from_secs(2)),
|
||||
CodexErr::InternalServerError.with_retry_delay(Duration::from_secs(3)),
|
||||
]
|
||||
.map(|err| format!("{err:?}"));
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
[
|
||||
"Timeout".to_string(),
|
||||
"Stream(\"disconnected\", None)".to_string(),
|
||||
"Stream(\"retry later\", Some(2s))".to_string(),
|
||||
"InternalServerError".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retryability_preserves_error_details_distinctions() {
|
||||
let errors = [
|
||||
(CodexErr::ServerOverloaded, false),
|
||||
(
|
||||
CodexErr::RetryLimit(RetryLimitReachedError {
|
||||
status: StatusCode::TOO_MANY_REQUESTS,
|
||||
request_id: None,
|
||||
}),
|
||||
false,
|
||||
),
|
||||
(
|
||||
CodexErr::UnexpectedStatus(UnexpectedResponseError {
|
||||
status: StatusCode::TOO_MANY_REQUESTS,
|
||||
body: String::new(),
|
||||
user_message: None,
|
||||
url: None,
|
||||
cf_ray: None,
|
||||
request_id: None,
|
||||
identity_authorization_error: None,
|
||||
identity_error_code: None,
|
||||
}),
|
||||
true,
|
||||
),
|
||||
(CodexErr::InternalServerError, true),
|
||||
];
|
||||
|
||||
for (err, expected) in errors {
|
||||
assert_eq!(
|
||||
err.is_retryable(),
|
||||
expected,
|
||||
"unexpected retryability for {err:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn rate_limit_snapshot() -> RateLimitSnapshot {
|
||||
let primary_reset_at = Utc
|
||||
|
||||
Reference in New Issue
Block a user