This commit is contained in:
Ahmed Ibrahim
2025-11-25 14:18:25 -08:00
parent 6b66534356
commit 53ff941cf3
6 changed files with 34 additions and 246 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -1144,6 +1144,7 @@ dependencies = [
"codex-apply-patch",
"codex-arg0",
"codex-async-utils",
"codex-client",
"codex-execpolicy",
"codex-file-search",
"codex-git",

View File

@@ -1,8 +1,8 @@
[package]
name = "codex-core"
version.workspace = true
edition.workspace = true
license.workspace = true
name = "codex-core"
version.workspace = true
[lib]
doctest = false
@@ -18,12 +18,13 @@ askama = { workspace = true }
async-channel = { workspace = true }
async-trait = { workspace = true }
base64 = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
chardetng = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
codex-api = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-apply-patch = { workspace = true }
codex-async-utils = { workspace = true }
codex-api = { workspace = true }
codex-client = { workspace = true }
codex-execpolicy = { workspace = true }
codex-file-search = { workspace = true }
codex-git = { workspace = true }
@@ -37,8 +38,8 @@ codex-utils-string = { workspace = true }
codex-windows-sandbox = { package = "codex-windows-sandbox", path = "../windows-sandbox-rs" }
dirs = { workspace = true }
dunce = { workspace = true }
env-flags = { workspace = true }
encoding_rs = { workspace = true }
env-flags = { workspace = true }
eventsource-stream = { workspace = true }
futures = { workspace = true }
http = { workspace = true }
@@ -46,8 +47,10 @@ indexmap = { workspace = true }
keyring = { workspace = true, features = ["crypto-rust"] }
libc = { workspace = true }
mcp-types = { workspace = true }
once_cell = { workspace = true }
os_info = { workspace = true }
rand = { workspace = true }
regex = { workspace = true }
regex-lite = { workspace = true }
reqwest = { workspace = true, features = ["json", "stream"] }
serde = { workspace = true, features = ["derive"] }
@@ -57,9 +60,6 @@ sha2 = { workspace = true }
shlex = { workspace = true }
similar = { workspace = true }
strum_macros = { workspace = true }
url = { workspace = true }
once_cell = { workspace = true }
regex = { workspace = true }
tempfile = { workspace = true }
test-case = "3.3.1"
test-log = { workspace = true }
@@ -83,15 +83,16 @@ toml_edit = { workspace = true }
tracing = { workspace = true, features = ["log"] }
tree-sitter = { workspace = true }
tree-sitter-bash = { workspace = true }
url = { workspace = true }
uuid = { workspace = true, features = ["serde", "v4", "v5"] }
which = { workspace = true }
wildmatch = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
keyring = { workspace = true, features = ["linux-native-async-persistent"] }
landlock = { workspace = true }
seccompiler = { workspace = true }
keyring = { workspace = true, features = ["linux-native-async-persistent"] }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.9"

View File

@@ -52,6 +52,7 @@ use serde_json::Value;
use tokio::sync::Mutex;
use tokio::sync::RwLock;
use tokio::sync::oneshot;
use tokio::time::sleep_until;
use tokio_util::sync::CancellationToken;
use tracing::debug;
use tracing::error;
@@ -106,6 +107,7 @@ use crate::shell;
use crate::state::ActiveTurn;
use crate::state::SessionServices;
use crate::state::SessionState;
use crate::status::IdleWarning;
use crate::tasks::GhostSnapshotTask;
use crate::tasks::ReviewTask;
use crate::tasks::SessionTask;
@@ -2187,6 +2189,8 @@ async fn try_run_turn(
.or_cancel(&cancellation_token)
.await??;
let mut idle_warning = IdleWarning::default();
let tool_runtime = ToolCallRuntime::new(
Arc::clone(&router),
Arc::clone(&sess),
@@ -2202,7 +2206,24 @@ async fn try_run_turn(
// Poll the next item from the model stream. We must inspect *both* Ok and Err
// cases so that transient stream failures (e.g., dropped SSE connection before
// `response.completed`) bubble up and trigger the caller's retry logic.
let event = match stream.next().or_cancel(&cancellation_token).await {
let event = tokio::select! {
biased;
result = stream.next().or_cancel(&cancellation_token) => result,
_ = sleep_until(idle_warning.deadline()) => {
if let Some(message) = idle_warning.maybe_warning_message().await {
sess.send_event(
&turn_context,
EventMsg::Warning(WarningEvent { message }),
)
.await;
}
continue;
}
};
idle_warning.mark_event();
let event = match event {
Ok(event) => event,
Err(codex_async_utils::CancelErr::Cancelled) => {
let processed_items = output.try_collect().await?;

View File

@@ -42,6 +42,7 @@ pub mod parse_command;
pub mod powershell;
mod response_processing;
pub mod sandboxing;
pub mod status;
mod text_encoding;
pub mod token_data;
mod truncate;

View File

@@ -1,19 +0,0 @@
[package]
name = "codex-status"
version.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
anyhow = { workspace = true }
codex-client = { path = "../codex-client" }
reqwest = { workspace = true, features = ["json"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }
serde_json = { workspace = true }

View File

@@ -1,217 +0,0 @@
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use anyhow::bail;
use codex_client::Request;
use codex_client::ReqwestTransport;
use http::header::CONTENT_TYPE;
use serde::Deserialize;
use serde::Serialize;
use std::time::Duration;
use strum::Display;
const STATUS_WIDGET_URL: &str = "https://status.openai.com/proxy/status.openai.com";
const CODEX_COMPONENT_NAME: &str = "Codex";
#[derive(Debug, Clone, Display, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ComponentHealth {
Operational,
DegradedPerformance,
PartialOutage,
MajorOutage,
UnderMaintenance,
#[serde(other)]
Unknown,
}
impl ComponentHealth {
fn operational() -> Self {
Self::Operational
}
pub fn is_operational(self) -> bool {
self == Self::Operational
}
}
pub async fn fetch_codex_health() -> Result<ComponentHealth> {
let client = reqwest::Client::builder()
.connect_timeout(Duration::from_secs(5))
.timeout(Duration::from_secs(10))
.build()
.context("building HTTP client")?;
let response = ReqwestTransport::new(client)
.execute(Request::new(
http::Method::GET,
STATUS_WIDGET_URL.to_string(),
))
.await
.context("requesting status widget")?;
let content_type = response
.headers
.get(CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.to_ascii_lowercase();
if !content_type.contains("json") {
let snippet = String::from_utf8_lossy(&response.body)
.chars()
.take(200)
.collect::<String>();
bail!(
"Expected JSON from {STATUS_WIDGET_URL}: Content-Type={content_type}. Body starts with: {snippet:?}"
);
}
let payload: StatusPayload =
serde_json::from_slice(&response.body).context("parsing status widget JSON")?;
derive_component_health(&payload, CODEX_COMPONENT_NAME)
}
#[derive(Debug, Clone, Deserialize)]
struct StatusPayload {
#[serde(default)]
summary: Summary,
}
#[derive(Debug, Clone, Deserialize)]
struct Summary {
#[serde(default)]
components: Vec<Component>,
#[serde(default)]
affected_components: Vec<AffectedComponent>,
}
#[derive(Debug, Clone, Deserialize)]
struct Component {
id: String,
name: String,
}
#[derive(Debug, Clone, Deserialize)]
struct AffectedComponent {
component_id: String,
#[serde(default = "ComponentHealth::operational")]
status: ComponentHealth,
}
fn derive_component_health(
payload: &StatusPayload,
component_name: &str,
) -> Result<ComponentHealth> {
let component = payload
.summary
.components
.iter()
.find(|component| component.name == component_name)
.ok_or_else(|| anyhow!("Component {component_name:?} not found in status summary"))?;
let status = payload
.summary
.affected_components
.iter()
.find(|affected| affected.component_id == component.id)
.map(|affected| affected.status)
.unwrap_or(ComponentHealth::Operational);
Ok(status)
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use serde_json::json;
#[test]
fn defaults_to_operational_when_not_affected() {
let payload = serde_json::from_value::<StatusPayload>(json!({
"summary": {
"id": "sum-1",
"name": "OpenAI",
"components": [
{"id": "cmp-1", "name": "Codex", "status_page_id": "page-1"},
{"id": "cmp-2", "name": "Chat", "status_page_id": "page-1"}
]
}
}))
.expect("valid payload");
let status = derive_component_health(&payload, "Codex").expect("codex component exists");
assert_eq!(status, ComponentHealth::Operational);
assert!(status.is_operational());
}
#[test]
fn uses_affected_component_status() {
let payload = serde_json::from_value::<StatusPayload>(json!({
"summary": {
"id": "sum-1",
"name": "OpenAI",
"components": [
{"id": "cmp-1", "name": "Codex", "status_page_id": "page-1"}
],
"affected_components": [
{"component_id": "cmp-1", "status": "major_outage"}
]
}
}))
.expect("valid payload");
let status = derive_component_health(&payload, "Codex").expect("codex component exists");
assert_eq!(status, ComponentHealth::MajorOutage);
assert!(!status.is_operational());
}
#[test]
fn unknown_status_is_preserved_as_unknown() {
let payload = serde_json::from_value::<StatusPayload>(json!({
"summary": {
"id": "sum-1",
"name": "OpenAI",
"components": [
{"id": "cmp-1", "name": "Codex", "status_page_id": "page-1"}
],
"affected_components": [
{"component_id": "cmp-1", "status": "custom_status"}
]
}
}))
.expect("valid payload");
let status = derive_component_health(&payload, "Codex").expect("codex component exists");
assert_eq!(status, ComponentHealth::Unknown);
assert!(!status.is_operational());
}
#[test]
fn missing_component_returns_error() {
let payload = serde_json::from_value::<StatusPayload>(json!({
"summary": {
"id": "sum-1",
"name": "OpenAI",
"components": [],
"affected_components": []
}
}))
.expect("valid payload");
let error =
derive_component_health(&payload, "Codex").expect_err("missing component should error");
assert!(
error
.to_string()
.contains("Component \"Codex\" not found in status summary")
);
}
}