[VSCE-157] Allow Link Formatting via Markdown in System Errors

This commit is contained in:
Colin Young
2025-10-07 18:48:18 -07:00
parent abd517091f
commit 0fc0137720
9 changed files with 159 additions and 21 deletions
+12 -5
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
+3 -7
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;
+92 -3
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 {
+1 -1
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!(