From 53ff941cf3a61b8b0d6e15000790c2caa3c85848 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Tue, 25 Nov 2025 14:18:25 -0800 Subject: [PATCH] warning --- codex-rs/Cargo.lock | 1 + codex-rs/core/Cargo.toml | 19 ++-- codex-rs/core/src/codex.rs | 23 +++- codex-rs/core/src/lib.rs | 1 + codex-rs/status/Cargo.toml | 19 ---- codex-rs/status/src/lib.rs | 217 ------------------------------------- 6 files changed, 34 insertions(+), 246 deletions(-) delete mode 100644 codex-rs/status/Cargo.toml delete mode 100644 codex-rs/status/src/lib.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 34ea8c8773..8e3ff0deb4 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1144,6 +1144,7 @@ dependencies = [ "codex-apply-patch", "codex-arg0", "codex-async-utils", + "codex-client", "codex-execpolicy", "codex-file-search", "codex-git", diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 705bab57df..d549a3c0f7 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -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" diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index e3c5de5762..b43e4df81d 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -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?; diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 7a9440eb28..23708145c0 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -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; diff --git a/codex-rs/status/Cargo.toml b/codex-rs/status/Cargo.toml deleted file mode 100644 index 89ea645840..0000000000 --- a/codex-rs/status/Cargo.toml +++ /dev/null @@ -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 } diff --git a/codex-rs/status/src/lib.rs b/codex-rs/status/src/lib.rs deleted file mode 100644 index 5c96be24bf..0000000000 --- a/codex-rs/status/src/lib.rs +++ /dev/null @@ -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 { - 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::(); - - 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, - #[serde(default)] - affected_components: Vec, -} - -#[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 { - 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::(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::(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::(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::(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") - ); - } -}