Compare commits

...

1 Commits

Author SHA1 Message Date
Colin Young
0fc0137720 [VSCE-157] Allow Link Formatting via Markdown in System Errors 2025-10-07 18:48:18 -07:00
9 changed files with 159 additions and 21 deletions

View File

@@ -48,6 +48,7 @@ use crate::conversation_history::ConversationHistory;
use crate::environment_context::EnvironmentContext;
use crate::error::CodexErr;
use crate::error::Result as CodexResult;
use crate::error::error_event_from;
use crate::exec::ExecToolCallOutput;
#[cfg(test)]
use crate::exec::StreamOutput;
@@ -388,7 +389,10 @@ impl Session {
error!("{message}");
post_session_configured_error_events.push(Event {
id: INITIAL_SUBMIT_ID.to_owned(),
msg: EventMsg::Error(ErrorEvent { message }),
msg: EventMsg::Error(ErrorEvent {
message,
markdown_message: None,
}),
});
(McpConnectionManager::default(), Default::default())
}
@@ -401,7 +405,10 @@ impl Session {
error!("{message}");
post_session_configured_error_events.push(Event {
id: INITIAL_SUBMIT_ID.to_owned(),
msg: EventMsg::Error(ErrorEvent { message }),
msg: EventMsg::Error(ErrorEvent {
message,
markdown_message: None,
}),
});
}
}
@@ -1458,6 +1465,7 @@ async fn submission_loop(
id: sub.id.clone(),
msg: EventMsg::Error(ErrorEvent {
message: "Failed to shutdown rollout recorder".to_string(),
markdown_message: None,
}),
};
sess.send_event(event).await;
@@ -1841,6 +1849,7 @@ pub(crate) async fn run_task(
message: format!(
"Conversation is still above the token limit after automatic summarization (limit {limit_str}, current {current_tokens}). Please start a new session or trim your input."
),
markdown_message: None,
}),
};
sess.send_event(event).await;
@@ -1871,9 +1880,7 @@ pub(crate) async fn run_task(
info!("Turn error: {e:#}");
let event = Event {
id: sub_id.clone(),
msg: EventMsg::Error(ErrorEvent {
message: e.to_string(),
}),
msg: EventMsg::Error(error_event_from(&e)),
};
sess.send_event(event).await;
// let the user continue the conversation

View File

@@ -7,9 +7,9 @@ use crate::Prompt;
use crate::client_common::ResponseEvent;
use crate::error::CodexErr;
use crate::error::Result as CodexResult;
use crate::error::error_event_from;
use crate::protocol::AgentMessageEvent;
use crate::protocol::CompactedItem;
use crate::protocol::ErrorEvent;
use crate::protocol::Event;
use crate::protocol::EventMsg;
use crate::protocol::InputItem;
@@ -108,9 +108,7 @@ async fn run_compact_task_inner(
.await;
let event = Event {
id: sub_id.clone(),
msg: EventMsg::Error(ErrorEvent {
message: e.to_string(),
}),
msg: EventMsg::Error(error_event_from(&e)),
};
sess.send_event(event).await;
return;
@@ -131,9 +129,7 @@ async fn run_compact_task_inner(
} else {
let event = Event {
id: sub_id.clone(),
msg: EventMsg::Error(ErrorEvent {
message: e.to_string(),
}),
msg: EventMsg::Error(error_event_from(&e)),
};
sess.send_event(event).await;
return;

View File

@@ -2,6 +2,7 @@ use crate::exec::ExecToolCallOutput;
use crate::token_data::KnownPlan;
use crate::token_data::PlanType;
use codex_protocol::ConversationId;
use codex_protocol::protocol::ErrorEvent;
use codex_protocol::protocol::RateLimitSnapshot;
use reqwest::StatusCode;
use serde_json;
@@ -12,6 +13,8 @@ use tokio::task::JoinError;
pub type Result<T> = std::result::Result<T, CodexErr>;
const PRICING_URL: &str = "https://openai.com/chatgpt/pricing";
#[derive(Error, Debug)]
pub enum SandboxErr {
/// Error from sandbox execution
@@ -197,7 +200,7 @@ impl std::fmt::Display for UsageLimitReachedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let message = match self.plan_type.as_ref() {
Some(PlanType::Known(KnownPlan::Plus)) => format!(
"You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing){}",
"You've hit your usage limit. Upgrade to Pro ({PRICING_URL}){}",
retry_suffix_after_or(self.resets_in_seconds)
),
Some(PlanType::Known(KnownPlan::Team)) | Some(PlanType::Known(KnownPlan::Business)) => {
@@ -207,8 +210,9 @@ impl std::fmt::Display for UsageLimitReachedError {
)
}
Some(PlanType::Known(KnownPlan::Free)) => {
"You've hit your usage limit. Upgrade to Plus to continue using Codex (https://openai.com/chatgpt/pricing)."
.to_string()
format!(
"You've hit your usage limit. Upgrade to Plus to continue using Codex ({PRICING_URL})."
)
}
Some(PlanType::Known(KnownPlan::Pro))
| Some(PlanType::Known(KnownPlan::Enterprise))
@@ -226,6 +230,40 @@ impl std::fmt::Display for UsageLimitReachedError {
}
}
impl UsageLimitReachedError {
pub(crate) fn markdown_message(&self) -> String {
match self.plan_type.as_ref() {
Some(PlanType::Known(KnownPlan::Plus)) => format!(
"You've hit your usage limit. Upgrade to [Pro]({PRICING_URL}){}",
retry_suffix_after_or(self.resets_in_seconds)
),
Some(PlanType::Known(KnownPlan::Free)) => format!(
"You've hit your usage limit. [Upgrade to Plus]({PRICING_URL}) to continue using Codex."
),
_ => self.to_string(),
}
}
}
impl CodexErr {
pub(crate) fn markdown_message(&self) -> Option<String> {
match self {
CodexErr::UsageLimitReached(err) => Some(err.markdown_message()),
CodexErr::UsageNotIncluded => Some(format!(
"To use Codex with your ChatGPT plan, [upgrade to Plus]({PRICING_URL})."
)),
_ => None,
}
}
}
pub(crate) fn error_event_from(err: &CodexErr) -> ErrorEvent {
ErrorEvent {
message: err.to_string(),
markdown_message: err.markdown_message(),
}
}
fn retry_suffix(resets_in_seconds: Option<u64>) -> String {
if let Some(secs) = resets_in_seconds {
let reset_duration = format_reset_duration(secs);
@@ -348,6 +386,19 @@ mod tests {
);
}
#[test]
fn usage_limit_reached_error_markdown_plus_plan() {
let err = UsageLimitReachedError {
plan_type: Some(PlanType::Known(KnownPlan::Plus)),
resets_in_seconds: Some(60),
rate_limits: Some(rate_limit_snapshot()),
};
assert_eq!(
err.markdown_message(),
"You've hit your usage limit. Upgrade to [Pro](https://openai.com/chatgpt/pricing) or try again in 1 minute."
);
}
#[test]
fn usage_limit_reached_error_formats_free_plan() {
let err = UsageLimitReachedError {
@@ -361,6 +412,19 @@ mod tests {
);
}
#[test]
fn usage_limit_reached_error_markdown_free_plan() {
let err = UsageLimitReachedError {
plan_type: Some(PlanType::Known(KnownPlan::Free)),
resets_in_seconds: Some(3600),
rate_limits: Some(rate_limit_snapshot()),
};
assert_eq!(
err.markdown_message(),
"You've hit your usage limit. [Upgrade to Plus](https://openai.com/chatgpt/pricing) to continue using Codex."
);
}
#[test]
fn usage_limit_reached_error_formats_default_when_none() {
let err = UsageLimitReachedError {
@@ -413,6 +477,31 @@ mod tests {
);
}
#[test]
fn codex_err_usage_not_included_markdown() {
let err = CodexErr::UsageNotIncluded;
assert_eq!(
err.markdown_message(),
Some("To use Codex with your ChatGPT plan, [upgrade to Plus](https://openai.com/chatgpt/pricing).".to_string())
);
}
#[test]
fn codex_err_usage_limit_reached_markdown() {
let err = CodexErr::UsageLimitReached(UsageLimitReachedError {
plan_type: Some(PlanType::Known(KnownPlan::Plus)),
resets_in_seconds: Some(120),
rate_limits: Some(rate_limit_snapshot()),
});
assert_eq!(
err.markdown_message(),
Some(
"You've hit your usage limit. Upgrade to [Pro](https://openai.com/chatgpt/pricing) or try again in 2 minutes."
.to_string()
)
);
}
#[test]
fn usage_limit_reached_includes_minutes_when_available() {
let err = UsageLimitReachedError {

View File

@@ -583,7 +583,7 @@ async fn auto_compact_stops_after_failed_attempt() {
.unwrap();
let error_event = wait_for_event(&codex, |ev| matches!(ev, EventMsg::Error(_))).await;
let EventMsg::Error(ErrorEvent { message }) = error_event else {
let EventMsg::Error(ErrorEvent { message, .. }) = error_event else {
panic!("expected error event");
};
assert!(

View File

@@ -157,7 +157,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}");
}

View File

@@ -434,6 +434,7 @@ fn error_event_produces_error() {
"e1",
EventMsg::Error(codex_core::protocol::ErrorEvent {
message: "boom".to_string(),
markdown_message: None,
}),
));
assert_eq!(
@@ -469,6 +470,7 @@ fn error_followed_by_task_complete_produces_turn_failed() {
"e1",
EventMsg::Error(ErrorEvent {
message: "boom".to_string(),
markdown_message: None,
}),
);
assert_eq!(

View File

@@ -15,6 +15,7 @@ use codex_core::NewConversation;
use codex_core::config::Config as CodexConfig;
use codex_core::protocol::AgentMessageEvent;
use codex_core::protocol::ApplyPatchApprovalRequestEvent;
use codex_core::protocol::ErrorEvent;
use codex_core::protocol::Event;
use codex_core::protocol::EventMsg;
use codex_core::protocol::ExecApprovalRequestEvent;
@@ -194,10 +195,12 @@ async fn run_codex_tool_session_inner(
}
EventMsg::Error(err_event) => {
// Return a response to conclude the tool call when the Codex session reports an error (e.g., interruption).
let result = json!({
"error": err_event.message,
});
let result = error_result_json(&err_event);
outgoing.send_response(request_id.clone(), result).await;
running_requests_id_to_codex_uuid
.lock()
.await
.remove(&request_id);
break;
}
EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
@@ -310,3 +313,41 @@ async fn run_codex_tool_session_inner(
}
}
}
fn error_result_json(err_event: &ErrorEvent) -> serde_json::Value {
let mut result = json!({
"error": err_event.message.clone(),
});
if let Some(markdown_message) = &err_event.markdown_message {
result["errorMarkdown"] = json!(markdown_message);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_result_without_markdown() {
let event = ErrorEvent {
message: "raw".to_string(),
markdown_message: None,
};
assert_eq!(error_result_json(&event), json!({ "error": "raw" }));
}
#[test]
fn error_result_with_markdown() {
let event = ErrorEvent {
message: "raw".to_string(),
markdown_message: Some("md".to_string()),
};
assert_eq!(
error_result_json(&event),
json!({
"error": "raw",
"errorMarkdown": "md",
})
);
}
}

View File

@@ -534,6 +534,9 @@ pub struct ExitedReviewModeEvent {
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
pub struct ErrorEvent {
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub markdown_message: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, TS)]

View File

@@ -1395,7 +1395,7 @@ impl ChatWidget {
self.set_token_info(ev.info);
self.on_rate_limit_snapshot(ev.rate_limits);
}
EventMsg::Error(ErrorEvent { message }) => self.on_error(message),
EventMsg::Error(ErrorEvent { message, .. }) => self.on_error(message),
EventMsg::TurnAborted(ev) => match ev.reason {
TurnAbortReason::Interrupted => {
self.on_interrupted_turn(ev.reason);