diff --git a/codex-rs/agent-message-board-host/src/lib.rs b/codex-rs/agent-message-board-host/src/lib.rs index fc60220721..c2f782da60 100644 --- a/codex-rs/agent-message-board-host/src/lib.rs +++ b/codex-rs/agent-message-board-host/src/lib.rs @@ -212,7 +212,7 @@ impl MessageBoardHost for LocalBoardHost { Box::pin(async move { let actor = self.actor(caller).await?; self.registered_path(&actor).await?; - actor.current_time().await + actor.current_time() }) } diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index af91b03abc..23499c4ca4 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs index 979eba47d8..a92644a655 100644 --- a/codex-rs/app-server-protocol/src/export.rs +++ b/codex-rs/app-server-protocol/src/export.rs @@ -2229,13 +2229,6 @@ mod tests { .ok_or_else(|| anyhow::anyhow!("missing MCP login completion fixture"))?, )?; assert!(mcp_login_completion_ts.contains("loginId?: string | null")); - let server_request_ts = std::str::from_utf8( - fixture_tree - .get(Path::new("ServerRequest.ts")) - .ok_or_else(|| anyhow::anyhow!("missing ServerRequest.ts fixture"))?, - )?; - assert_eq!(server_request_ts.contains("currentTime/read"), false); - assert_eq!(server_request_ts.contains("CurrentTimeReadParams"), false); let typescript_index = std::str::from_utf8( fixture_tree .get(Path::new("index.ts")) @@ -2256,14 +2249,6 @@ mod tests { fixture_tree.contains_key(Path::new("v2/MockExperimentalMethodResponse.ts")), false ); - assert_eq!( - fixture_tree.contains_key(Path::new("v2/CurrentTimeReadParams.ts")), - false - ); - assert_eq!( - fixture_tree.contains_key(Path::new("v2/CurrentTimeReadResponse.ts")), - false - ); assert_eq!( fixture_tree.contains_key(Path::new("v2/RemoteControlClient.ts")), false diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 16c96b79ad..be2fc3f0e9 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1943,13 +1943,6 @@ server_request_definitions! { response: v2::AttestationGenerateResponse, }, - #[experimental("currentTime/read")] - /// Read the current time from an external clock owned by the client. - CurrentTimeRead => "currentTime/read" { - params: v2::CurrentTimeReadParams, - response: v2::CurrentTimeReadResponse, - }, - /// DEPRECATED APIs below /// Request to approve a patch. /// This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage). @@ -3131,32 +3124,6 @@ mod tests { Ok(()) } - #[test] - fn serialize_current_time_read_request() -> Result<()> { - let params = v2::CurrentTimeReadParams { - thread_id: "thread-123".to_string(), - }; - let request = ServerRequest::CurrentTimeRead { - request_id: RequestId::Integer(10), - params: params.clone(), - }; - assert_eq!( - json!({ - "method": "currentTime/read", - "id": 10, - "params": { - "threadId": "thread-123" - } - }), - serde_json::to_value(&request)?, - ); - - let payload = ServerRequestPayload::CurrentTimeRead(params); - assert_eq!(request.id(), &RequestId::Integer(10)); - assert_eq!(payload.request_with_id(RequestId::Integer(10)), request); - Ok(()) - } - #[test] fn serialize_server_response() -> Result<()> { let response = ServerResponse::CommandExecutionRequestApproval { diff --git a/codex-rs/app-server-protocol/src/protocol/v2/current_time.rs b/codex-rs/app-server-protocol/src/protocol/v2/current_time.rs deleted file mode 100644 index 830b05b862..0000000000 --- a/codex-rs/app-server-protocol/src/protocol/v2/current_time.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::JsonSchema; -use crate::TS; -use serde::Deserialize; -use serde::Serialize; - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CurrentTimeReadParams { - pub thread_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CurrentTimeReadResponse { - /// Current time as whole Unix seconds. - #[ts(type = "number")] - pub current_time_at: i64, -} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/mod.rs b/codex-rs/app-server-protocol/src/protocol/v2/mod.rs index 96bb11c2aa..77e2c34ffb 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/mod.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/mod.rs @@ -10,7 +10,6 @@ mod collaboration_mode; mod command_exec; mod computer_use_config; mod config; -mod current_time; mod diagnostics; mod environment; mod experimental_feature; @@ -54,7 +53,6 @@ pub use collaboration_mode::*; pub use command_exec::*; pub use computer_use_config::*; pub use config::*; -pub use current_time::*; pub use diagnostics::*; pub use environment::*; pub use experimental_feature::*; diff --git a/codex-rs/app-server/src/current_time.rs b/codex-rs/app-server/src/current_time.rs deleted file mode 100644 index 57be618af3..0000000000 --- a/codex-rs/app-server/src/current_time.rs +++ /dev/null @@ -1,177 +0,0 @@ -use std::sync::Arc; -use std::sync::Weak; - -use anyhow::Context; -use anyhow::Result; -use anyhow::anyhow; -use anyhow::bail; -use chrono::DateTime; -use chrono::Utc; -use codex_app_server_protocol::CurrentTimeReadParams; -use codex_app_server_protocol::CurrentTimeReadResponse; -use codex_app_server_protocol::ServerRequestPayload; -use codex_core::SleepFuture; -use codex_core::TimeFuture; -use codex_core::TimeProvider; -use codex_protocol::ThreadId; -use tokio::time::Duration; -use tokio::time::Instant; -use tokio::time::timeout_at; - -use crate::outgoing_message::ConnectionId; -use crate::outgoing_message::OutgoingMessageSender; -use crate::thread_state::ThreadStateManager; - -const CURRENT_TIME_REQUEST_TIMEOUT: Duration = Duration::from_secs(10); -const CURRENT_TIME_POLL_INTERVAL: Duration = Duration::from_secs(1); - -pub(crate) fn app_server_time_provider( - outgoing: Arc, - thread_state_manager: ThreadStateManager, -) -> Arc { - Arc::new(AppServerTimeProvider { - outgoing: Arc::downgrade(&outgoing), - thread_state_manager, - }) -} - -struct AppServerTimeProvider { - outgoing: Weak, - thread_state_manager: ThreadStateManager, -} - -impl TimeProvider for AppServerTimeProvider { - fn current_time(&self, thread_id: ThreadId) -> TimeFuture<'_> { - let outgoing = self.outgoing.clone(); - let thread_state_manager = self.thread_state_manager.clone(); - Box::pin(async move { - let outgoing = outgoing - .upgrade() - .context("app-server current-time provider is unavailable")?; - request_current_time(outgoing, thread_state_manager, thread_id).await - }) - } - - fn sleep(&self, thread_id: ThreadId, duration: Duration) -> SleepFuture<'_> { - let outgoing = self.outgoing.clone(); - let thread_state_manager = self.thread_state_manager.clone(); - Box::pin(async move { - let outgoing = outgoing - .upgrade() - .context("app-server current-time provider is unavailable")?; - let started_at = - request_current_time(outgoing.clone(), thread_state_manager.clone(), thread_id) - .await?; - let wake_at = started_at - .checked_add_signed( - chrono::Duration::from_std(duration) - .context("external sleep duration is outside the supported range")?, - ) - .context("external sleep deadline is outside the supported range")?; - - loop { - tokio::time::sleep(CURRENT_TIME_POLL_INTERVAL).await; - if request_current_time(outgoing.clone(), thread_state_manager.clone(), thread_id) - .await? - >= wake_at - { - return Ok(()); - } - } - }) - } -} - -async fn request_current_time( - outgoing: Arc, - thread_state_manager: ThreadStateManager, - thread_id: ThreadId, -) -> Result> { - let deadline = Instant::now() + CURRENT_TIME_REQUEST_TIMEOUT; - timeout_at( - deadline, - thread_state_manager.wait_for_thread_subscriber(thread_id), - ) - .await - .map_err(|_| { - anyhow!( - "timed out waiting for a client to subscribe to the thread after {}s", - CURRENT_TIME_REQUEST_TIMEOUT.as_secs() - ) - })?; - let connection_ids = thread_state_manager - .subscribed_connection_ids(thread_id) - .await; - let connection_id = require_single_current_time_connection(&connection_ids)?; - let connection_ids = [connection_id]; - let (request_id, rx) = outgoing - .send_request_to_connections( - Some(&connection_ids), - ServerRequestPayload::CurrentTimeRead(CurrentTimeReadParams { - thread_id: thread_id.to_string(), - }), - /*thread_id*/ None, - ) - .await; - - let result = match timeout_at(deadline, rx).await { - Ok(Ok(Ok(result))) => result, - Ok(Ok(Err(err))) => { - bail!( - "current-time request failed: code={} message={}", - err.code, - err.message - ); - } - Ok(Err(err)) => bail!("current-time request was canceled: {err}"), - Err(_) => { - let _canceled = outgoing.cancel_request(&request_id).await; - bail!( - "current-time request timed out after {}s", - CURRENT_TIME_REQUEST_TIMEOUT.as_secs() - ); - } - }; - let response: CurrentTimeReadResponse = - serde_json::from_value(result).context("invalid current-time response")?; - - DateTime::from_timestamp(response.current_time_at, 0) - .ok_or_else(|| anyhow!("current-time response is outside the supported range")) -} - -fn require_single_current_time_connection(connection_ids: &[ConnectionId]) -> Result { - // External clocks are not interchangeable, so do not choose one silently. - match connection_ids { - [connection_id] => Ok(*connection_id), - _ => bail!( - "expected exactly one client subscribed to the thread, found {}", - connection_ids.len() - ), - } -} - -#[cfg(test)] -mod tests { - use super::require_single_current_time_connection; - use crate::outgoing_message::ConnectionId; - - #[test] - fn current_time_connection_must_be_unambiguous() { - assert_eq!( - require_single_current_time_connection(&[ConnectionId(7)]).unwrap(), - ConnectionId(7) - ); - assert_eq!( - require_single_current_time_connection(&[]) - .unwrap_err() - .to_string(), - "expected exactly one client subscribed to the thread, found 0" - ); - assert_eq!( - require_single_current_time_connection(&[ConnectionId(7), ConnectionId(8)]) - .unwrap_err() - .to_string(), - "expected exactly one client subscribed to the thread, found 2" - ); - } -} diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index 7a2892b598..fcf03fc607 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -116,7 +116,6 @@ mod config_manager; mod config_manager_service; mod connection_cleanup; mod connection_rpc_gate; -mod current_time; mod daemon_thread_recovery; mod dynamic_tools; mod effective_plugin_change; diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 33cf7b80c3..70938064bf 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -8,7 +8,6 @@ use std::sync::atomic::AtomicBool; use crate::attestation::app_server_attestation_provider; use crate::config_manager::ConfigManager; use crate::connection_rpc_gate::ConnectionRpcGate; -use crate::current_time::app_server_time_provider; use crate::error_code::internal_error; use crate::error_code::invalid_params; use crate::error_code::invalid_request; @@ -417,11 +416,7 @@ impl MessageProcessor { outgoing.clone(), thread_state_manager.clone(), )), - { - let time_provider = - app_server_time_provider(outgoing.clone(), thread_state_manager.clone()); - Some(time_provider) - }, + None, ); match code_mode_session_provider { Some(provider) => manager.with_code_mode_session_provider(provider), diff --git a/codex-rs/app-server/tests/suite/v2/current_time.rs b/codex-rs/app-server/tests/suite/v2/current_time.rs index d311acd9a7..ed868f2369 100644 --- a/codex-rs/app-server/tests/suite/v2/current_time.rs +++ b/codex-rs/app-server/tests/suite/v2/current_time.rs @@ -1,126 +1,40 @@ +//! Explicit external time requires a native clock on the app-server. use anyhow::Result; use app_test_support::MockResponsesConfig; use app_test_support::TestAppServer; -use app_test_support::create_final_assistant_message_sse_response; -use chrono::DateTime; -use chrono::Local; -use chrono::Utc; -use codex_app_server_protocol::ClientRequest; -use codex_app_server_protocol::CurrentTimeReadResponse; -use codex_app_server_protocol::JSONRPCMessage; -use codex_app_server_protocol::ServerRequest; +use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ThreadStartParams; -use codex_app_server_protocol::ThreadStartResponse; -use codex_app_server_protocol::TurnStartParams; -use codex_app_server_protocol::TurnStartResponse; -use codex_app_server_protocol::UserInput; use core_test_support::responses; use core_test_support::skip_if_no_network; -use pretty_assertions::assert_eq; use tempfile::TempDir; -use tokio::time::Duration; -use tokio::time::timeout; - -#[cfg(windows)] -const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(25); -#[cfg(not(windows))] -const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10); -const CURRENT_TIME_AT: i64 = 1_781_717_655; -const CURRENT_TIME_REMINDER: &str = - "It is 2026-06-17 17:34:15 UTC."; #[tokio::test] -async fn current_time_read_round_trip_adds_reminder_to_model_input() -> Result<()> { +async fn external_time_without_native_provider_fails_thread_start() -> Result<()> { skip_if_no_network!(Ok(())); - let server = responses::start_mock_server().await; - let response_mock = responses::mount_sse_once( - &server, - create_final_assistant_message_sse_response("Done")?, - ) - .await; - let codex_home = TempDir::new()?; + let home = TempDir::new()?; MockResponsesConfig::new(&server.uri()) .with_extra_config( - r#"[features.current_time_reminder] -enabled = true -reminder_interval_seconds = 1 -clock_source = "external" -"#, + "[features.current_time_reminder]\nenabled = true\nclock_source = \"external\"", ) - .write(codex_home.path())?; - + .write(home.path())?; let mut app_server = TestAppServer::builder() - .with_codex_home(codex_home.path()) + .with_codex_home(home.path()) .build_initialized() .await?; - - let ThreadStartResponse { thread, .. } = app_server - .start_thread(ThreadStartParams::default()) + let id = app_server + .send_thread_start_request_with_auto_env(ThreadStartParams::default()) .await?; - - let _: TurnStartResponse = app_server - .request(|request_id| ClientRequest::TurnStart { - request_id, - params: TurnStartParams { - thread_id: thread.id.clone(), - input: vec![UserInput::Text { - text: "What time is it?".to_string(), - text_elements: Vec::new(), - }], - ..Default::default() - }, - }) + let error = app_server + .read_stream_until_error_message(RequestId::Integer(id)) .await?; - - let mut current_time_reads = 0; - timeout(DEFAULT_READ_TIMEOUT, async { - loop { - match app_server.read_next_message().await? { - JSONRPCMessage::Request(request) => { - let server_request = ServerRequest::try_from(request)?; - let ServerRequest::CurrentTimeRead { request_id, params } = server_request - else { - panic!("expected CurrentTimeRead request, got: {server_request:?}"); - }; - assert_eq!(params.thread_id, thread.id); - current_time_reads += 1; - app_server - .send_response( - request_id, - serde_json::to_value(CurrentTimeReadResponse { - current_time_at: CURRENT_TIME_AT, - })?, - ) - .await?; - } - JSONRPCMessage::Notification(notification) - if notification.method == "turn/completed" => - { - break Ok::<_, anyhow::Error>(()); - } - _ => {} - } - } - }) - .await??; - assert!(current_time_reads >= 2); - - let request = response_mock.single_request(); assert!( - request - .message_input_texts("developer") - .iter() - .any(|text| text == CURRENT_TIME_REMINDER) + error + .error + .message + .contains("no external current-time provider"), + "{}", + error.error.message ); - let current_date = DateTime::::from_timestamp(CURRENT_TIME_AT, 0) - .expect("test timestamp should be valid") - .with_timezone(&Local) - .format("%Y-%m-%d") - .to_string(); - assert!(request.message_input_texts("user").iter().any(|text| { - text.contains("") - && text.contains(&format!("{current_date}")) - })); Ok(()) } diff --git a/codex-rs/app-server/tests/suite/v2/sleep.rs b/codex-rs/app-server/tests/suite/v2/sleep.rs index b515dbce08..66e5d7ea01 100644 --- a/codex-rs/app-server/tests/suite/v2/sleep.rs +++ b/codex-rs/app-server/tests/suite/v2/sleep.rs @@ -2,12 +2,7 @@ use anyhow::Result; use app_test_support::MockResponsesConfig; use app_test_support::TestAppServer; use codex_app_server_protocol::ClientRequest; -use codex_app_server_protocol::CurrentTimeReadResponse; -use codex_app_server_protocol::ItemCompletedNotification; use codex_app_server_protocol::ItemStartedNotification; -use codex_app_server_protocol::JSONRPCErrorError; -use codex_app_server_protocol::ServerRequest; -use codex_app_server_protocol::SleepItem; use codex_app_server_protocol::ThreadItemPayload; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; @@ -21,7 +16,6 @@ use pretty_assertions::assert_eq; use serde_json::json; use std::time::Duration; use tempfile::TempDir; -use test_case::test_case; use tokio::time::timeout; use super::analytics::captured_analytics_events; @@ -32,7 +26,6 @@ use super::analytics::wait_for_matching_analytics_event; const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(25); #[cfg(not(windows))] const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10); -const CURRENT_TIME_AT: i64 = 1_781_717_655; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn clock_tools_emit_control_tool_analytics() -> Result<()> { @@ -163,157 +156,6 @@ async fn clock_tools_emit_control_tool_analytics() -> Result<()> { Ok(()) } -#[test_case(0; "initial read fails")] -#[test_case(1; "polling read fails")] -#[test_case(3; "successful reads")] -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn external_sleep_polls_current_time_and_emits_items( - successful_sleep_reads: usize, -) -> Result<()> { - const CALL_ID: &str = "sleep-1"; - const DURATION_MS: u64 = 2_000; - let read_fails = successful_sleep_reads < 3; - - let server = responses::start_mock_server().await; - let model = responses::mount_sse_sequence( - &server, - vec![ - responses::sse(vec![ - responses::ev_response_created("resp-1"), - responses::ev_function_call_with_namespace( - CALL_ID, - "clock", - "sleep", - &serde_json::json!({ "duration_ms": DURATION_MS }).to_string(), - ), - responses::ev_completed("resp-1"), - ]), - responses::sse(vec![ - responses::ev_assistant_message("msg-1", "Done"), - responses::ev_completed("resp-2"), - ]), - ], - ) - .await; - - let codex_home = TempDir::new()?; - MockResponsesConfig::new(&server.uri()) - .with_root_config("include_environment_context = false") - .with_extra_config(&format!( - r#"[features] -nonfatal_clock_read_errors = {read_fails} -[features.current_time_reminder] -enabled = true -sleep_tool = true -clock_source = "external" -"#, - )) - .write(codex_home.path())?; - - let mut mcp = TestAppServer::builder() - .with_codex_home(codex_home.path()) - .build_initialized() - .await?; - - let ThreadStartResponse { thread, .. } = mcp - .start_thread(ThreadStartParams { - model: Some("mock-model".to_string()), - ..Default::default() - }) - .await?; - - let TurnStartResponse { turn, .. } = mcp - .request(|request_id| ClientRequest::TurnStart { - request_id, - params: TurnStartParams { - thread_id: thread.id.clone(), - client_user_message_id: None, - input: vec![V2UserInput::Text { - text: "Sleep briefly".to_string(), - text_elements: Vec::new(), - }], - ..Default::default() - }, - }) - .await?; - - // Read once for the initial reminder, then once to establish the sleep deadline. - respond_to_current_time_read(&mut mcp, &thread.id, CURRENT_TIME_AT).await?; - let started = wait_for_sleep_started(&mut mcp, CALL_ID).await?; - for current_time_at in [CURRENT_TIME_AT, CURRENT_TIME_AT + 1, CURRENT_TIME_AT + 2] - .into_iter() - .take(successful_sleep_reads) - { - respond_to_current_time_read(&mut mcp, &thread.id, current_time_at).await?; - } - if read_fails { - let request = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_request_message(), - ) - .await??; - let ServerRequest::CurrentTimeRead { request_id, params } = request else { - panic!("expected clock read, got {request:?}"); - }; - assert_eq!(params.thread_id, thread.id); - mcp.send_error( - request_id, - JSONRPCErrorError { - code: -32_000, - message: "PRIVATE_CLOCK_ERROR".to_string(), - data: None, - }, - ) - .await?; - } - - let completed = wait_for_sleep_completed(&mut mcp, CALL_ID).await?; - - // The next inference boundary reads the same external clock after the tool returns. - respond_to_current_time_read(&mut mcp, &thread.id, CURRENT_TIME_AT + 2).await?; - let turn_completed: TurnCompletedNotification = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_notification("turn/completed"), - ) - .await??; - assert_eq!(turn_completed.turn.status, TurnStatus::Completed); - if read_fails { - let requests = model.requests(); - assert_eq!(requests.len(), 2); - assert_eq!( - requests[1].function_call_output_text(CALL_ID).as_deref(), - Some("failed to read current time") - ); - } - - let expected_item = ThreadItemPayload::Sleep(SleepItem { - id: CALL_ID.to_string(), - duration_ms: DURATION_MS, - }); - assert!(completed.completed_at_ms >= started.started_at_ms); - assert_eq!( - started, - ItemStartedNotification::new( - thread.id.clone(), - turn.id.clone(), - expected_item.clone(), - started.started_at_ms - ) - ); - assert_eq!( - completed, - ItemCompletedNotification::new( - thread.id, - turn.id, - expected_item, - Some(started.started_at_ms), - completed.completed_at_ms - ) - ); - - Ok(()) -} - async fn wait_for_sleep_started( mcp: &mut TestAppServer, call_id: &str, @@ -326,41 +168,3 @@ async fn wait_for_sleep_started( } } } - -async fn wait_for_sleep_completed( - mcp: &mut TestAppServer, - call_id: &str, -) -> Result { - loop { - let completed: ItemCompletedNotification = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_notification("item/completed"), - ) - .await??; - if matches!(&completed.item.payload, ThreadItemPayload::Sleep(item) if item.id == call_id) { - return Ok(completed); - } - } -} - -async fn respond_to_current_time_read( - mcp: &mut TestAppServer, - thread_id: &str, - current_time_at: i64, -) -> Result<()> { - let request = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_request_message(), - ) - .await??; - let ServerRequest::CurrentTimeRead { request_id, params } = request else { - panic!("expected CurrentTimeRead request, got: {request:?}"); - }; - assert_eq!(params.thread_id, thread_id); - mcp.send_response( - request_id, - serde_json::to_value(CurrentTimeReadResponse { current_time_at })?, - ) - .await?; - Ok(()) -} diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 0e8bde3dde..87fe12e8de 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -278,12 +278,11 @@ impl CodexThread { } /// Reads this thread's configured clock. - pub async fn current_time(&self) -> CodexResult> { + pub fn current_time(&self) -> CodexResult> { self.session .services .time_provider .current_time(self.session.thread_id) - .await .map_err(|error| CodexErr::Io(std::io::Error::other(error))) } diff --git a/codex-rs/core/src/current_time.rs b/codex-rs/core/src/current_time.rs index b2c3fadd79..6074026551 100644 --- a/codex-rs/core/src/current_time.rs +++ b/codex-rs/core/src/current_time.rs @@ -12,12 +12,12 @@ use codex_protocol::ThreadId; use crate::config::CurrentTimeReminderConfig; -pub type TimeFuture<'a> = Pin>> + Send + 'a>>; pub type SleepFuture<'a> = Pin> + Send + 'a>>; /// Host integration boundary for reading and waiting on the current time. pub trait TimeProvider: Send + Sync { - fn current_time(&self, thread_id: ThreadId) -> TimeFuture<'_>; + /// Reads the clock without yielding or calling back into the session. + fn current_time(&self, thread_id: ThreadId) -> Result>; /// Waits for the given duration on this provider's clock. /// @@ -28,8 +28,8 @@ pub trait TimeProvider: Send + Sync { pub(crate) struct SystemTimeProvider; impl TimeProvider for SystemTimeProvider { - fn current_time(&self, _thread_id: ThreadId) -> TimeFuture<'_> { - Box::pin(async { Ok(Utc::now()) }) + fn current_time(&self, _thread_id: ThreadId) -> Result> { + Ok(Utc::now()) } fn sleep(&self, _thread_id: ThreadId, duration: Duration) -> SleepFuture<'_> { @@ -53,3 +53,7 @@ pub(crate) fn resolve_time_provider( }), } } + +#[cfg(test)] +#[path = "current_time_tests.rs"] +mod tests; diff --git a/codex-rs/core/src/current_time_tests.rs b/codex-rs/core/src/current_time_tests.rs new file mode 100644 index 0000000000..02e5de3dc7 --- /dev/null +++ b/codex-rs/core/src/current_time_tests.rs @@ -0,0 +1,22 @@ +#![cfg(test)] + +use super::*; + +#[test] +fn explicit_external_time_requires_a_provider() { + let config = CurrentTimeReminderConfig { + clock_source: CurrentTimeSource::External, + ..Default::default() + }; + let err = resolve_time_provider(Some(&config), /*external_provider*/ None) + .err() + .expect("missing provider"); + assert!( + err.to_string() + .contains("no external current-time provider") + ); + let provider: Arc = Arc::new(SystemTimeProvider); + let selected = resolve_time_provider(Some(&config), Some(Arc::clone(&provider))).unwrap(); + assert!(Arc::ptr_eq(&selected, &provider)); + assert!(resolve_time_provider(/*config*/ None, /*external_provider*/ None).is_ok()); +} diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 109cb6e4dd..dc7d756ac1 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -281,7 +281,6 @@ pub use codex_prompts::REVIEW_PROMPT; pub use codex_runtime_agent_instance::resolve_installation_id; pub use compact::content_items_to_text; pub use current_time::SleepFuture; -pub use current_time::TimeFuture; pub use current_time::TimeProvider; pub use event_mapping::parse_turn_item; pub use exec_policy::ExecPolicyError; diff --git a/codex-rs/core/src/session/time_reminder.rs b/codex-rs/core/src/session/time_reminder.rs index 02f1cecb74..f6088b8fa9 100644 --- a/codex-rs/core/src/session/time_reminder.rs +++ b/codex-rs/core/src/session/time_reminder.rs @@ -102,12 +102,7 @@ impl Session { turn_context: &TurnContext, clock_read: &'static str, ) -> CodexResult>> { - let error = match self - .services - .time_provider - .current_time(self.thread_id) - .await - { + let error = match self.services.time_provider.current_time(self.thread_id) { Ok(time) => { let mut state = self.state.lock().await; state.current_time_reminder.last_clock_failure = None; diff --git a/codex-rs/core/src/tools/handlers/current_time.rs b/codex-rs/core/src/tools/handlers/current_time.rs index d00b1492ce..3e8d047c0c 100644 --- a/codex-rs/core/src/tools/handlers/current_time.rs +++ b/codex-rs/core/src/tools/handlers/current_time.rs @@ -103,7 +103,6 @@ impl ToolExecutor for CurrentTimeHandler { .services .time_provider .current_time(invocation.session.thread_id) - .await .map_err(|err| { if invocation .turn diff --git a/codex-rs/core/tests/suite/current_time_reminder.rs b/codex-rs/core/tests/suite/current_time_reminder.rs index 17c9eb05cf..3699ea25fd 100644 --- a/codex-rs/core/tests/suite/current_time_reminder.rs +++ b/codex-rs/core/tests/suite/current_time_reminder.rs @@ -14,7 +14,6 @@ use chrono::Local; use chrono::Utc; use codex_config::test_support::CloudConfigBundleFixture; use codex_core::SleepFuture; -use codex_core::TimeFuture; use codex_core::TimeProvider; use codex_core::TurnInputRequest; use codex_core::config::CurrentTimeReminderConfig; @@ -87,7 +86,7 @@ impl Default for TestTimeProvider { } impl TimeProvider for TestTimeProvider { - fn current_time(&self, _thread_id: ThreadId) -> TimeFuture<'_> { + fn current_time(&self, _thread_id: ThreadId) -> anyhow::Result> { let timestamp = if let Some(script) = &self.scripted_current_time { script .lock() @@ -97,11 +96,9 @@ impl TimeProvider for TestTimeProvider { } else { Ok(self.current_time.fetch_add(60, Ordering::Relaxed)) }; - Box::pin(async move { - let timestamp = timestamp.map_err(anyhow::Error::msg)?; - Ok(DateTime::::from_timestamp(timestamp, 0) - .expect("test timestamp should be valid")) - }) + + let timestamp = timestamp.map_err(anyhow::Error::msg)?; + Ok(DateTime::::from_timestamp(timestamp, 0).expect("test timestamp should be valid")) } fn sleep(&self, _thread_id: ThreadId, duration: Duration) -> SleepFuture<'_> { @@ -114,8 +111,8 @@ impl TimeProvider for TestTimeProvider { struct FailingTimeProvider; impl TimeProvider for FailingTimeProvider { - fn current_time(&self, _thread_id: ThreadId) -> TimeFuture<'_> { - Box::pin(async { Err(anyhow!("test clock unavailable")) }) + fn current_time(&self, _thread_id: ThreadId) -> anyhow::Result> { + Err(anyhow!("test clock unavailable")) } fn sleep(&self, _thread_id: ThreadId, _duration: Duration) -> SleepFuture<'_> { diff --git a/codex-rs/core/tests/suite/guardian_review.rs b/codex-rs/core/tests/suite/guardian_review.rs index 6ef2177fe0..27c112f70b 100644 --- a/codex-rs/core/tests/suite/guardian_review.rs +++ b/codex-rs/core/tests/suite/guardian_review.rs @@ -8,7 +8,6 @@ use chrono::Utc; use codex_config::types::McpServerConfig; use codex_config::types::OtelExporterKind; use codex_core::SleepFuture; -use codex_core::TimeFuture; use codex_core::TimeProvider; use codex_core::TurnInputRequest; use codex_core::config::Config; @@ -128,15 +127,14 @@ impl ToolLifecycleContributor for RecordingToolLifecycleContributor { } impl TimeProvider for RecordingTimeProvider { - fn current_time(&self, thread_id: ThreadId) -> TimeFuture<'_> { + fn current_time(&self, thread_id: ThreadId) -> anyhow::Result> { self.thread_ids .lock() .expect("time-provider thread ids lock should not be poisoned") .push(thread_id); - Box::pin(async { - Ok(DateTime::::from_timestamp(CURRENT_TIME_AT, 0) - .expect("test timestamp should be valid")) - }) + + Ok(DateTime::::from_timestamp(CURRENT_TIME_AT, 0) + .expect("test timestamp should be valid")) } fn sleep(&self, _thread_id: ThreadId, _duration: Duration) -> SleepFuture<'_> { diff --git a/codex-rs/core/tests/suite/scenarios_agent_message_board.rs b/codex-rs/core/tests/suite/scenarios_agent_message_board.rs index b2e7a21f27..258da2debd 100644 --- a/codex-rs/core/tests/suite/scenarios_agent_message_board.rs +++ b/codex-rs/core/tests/suite/scenarios_agent_message_board.rs @@ -42,16 +42,17 @@ enum BoardClock { } impl codex_core::TimeProvider for BoardClock { - fn current_time(&self, _thread_id: codex_protocol::ThreadId) -> codex_core::TimeFuture<'_> { - Box::pin(async move { - match self { - Self::Available => Ok(chrono::DateTime::parse_from_rfc3339( - "2026-09-18T12:00:00Z", - )? - .with_timezone(&chrono::Utc)), - Self::Unavailable => Err(anyhow::anyhow!("board clock unavailable")), - } - }) + fn current_time( + &self, + _thread_id: codex_protocol::ThreadId, + ) -> anyhow::Result> { + match self { + Self::Available => Ok( + chrono::DateTime::parse_from_rfc3339("2026-09-18T12:00:00Z")? + .with_timezone(&chrono::Utc), + ), + Self::Unavailable => Err(anyhow::anyhow!("board clock unavailable")), + } } fn sleep( diff --git a/codex-rs/core/tests/suite/subagent_notifications.rs b/codex-rs/core/tests/suite/subagent_notifications.rs index d5634b3be0..3573c017d5 100644 --- a/codex-rs/core/tests/suite/subagent_notifications.rs +++ b/codex-rs/core/tests/suite/subagent_notifications.rs @@ -1719,12 +1719,14 @@ async fn spawned_full_history_v2_child_uses_model_precedence_without_dropping_co struct FailFirstClockRead(std::sync::atomic::AtomicBool); impl codex_core::TimeProvider for FailFirstClockRead { - fn current_time(&self, _thread_id: ThreadId) -> codex_core::TimeFuture<'_> { + fn current_time( + &self, + _thread_id: ThreadId, + ) -> anyhow::Result> { let already_read = self.0.swap(true, std::sync::atomic::Ordering::Relaxed); - Box::pin(async move { - anyhow::ensure!(already_read, "parent clock unavailable"); - Ok(chrono::Utc::now()) - }) + + anyhow::ensure!(already_read, "parent clock unavailable"); + Ok(chrono::Utc::now()) } fn sleep( diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 3479ea4bcd..be28295458 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -2315,15 +2315,6 @@ async fn handle_server_request( ) .await } - ServerRequest::CurrentTimeRead { request_id, .. } => { - reject_server_request( - client, - request_id, - &method, - "external current time is not supported in exec mode".to_string(), - ) - .await - } ServerRequest::ApplyPatchApproval { request_id, params } => { reject_server_request( client, diff --git a/codex-rs/tui/src/app/agents_overview_tests.rs b/codex-rs/tui/src/app/agents_overview_tests.rs index 1ea48a6a60..c7f2ff1b39 100644 --- a/codex-rs/tui/src/app/agents_overview_tests.rs +++ b/codex-rs/tui/src/app/agents_overview_tests.rs @@ -146,7 +146,7 @@ use crate::test_support::test_path_display; use codex_app_server_client::AppServerClient; use codex_app_server_client::AppServerEvent; use codex_app_server_client::CloudClient; -use codex_app_server_protocol::CurrentTimeReadParams; +use codex_app_server_protocol::DynamicToolCallParams; use codex_app_server_protocol::ReasoningSummaryTextDeltaNotification; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerRequest; @@ -2016,10 +2016,15 @@ async fn root_switch_preserves_idle_root_with_running_subagent() -> Result<()> { ); app.handle_app_server_event( &app_server, - AppServerEvent::ServerRequest(Box::new(ServerRequest::CurrentTimeRead { + AppServerEvent::ServerRequest(Box::new(ServerRequest::DynamicToolCall { request_id: RequestId::Integer(99), - params: CurrentTimeReadParams { + params: DynamicToolCallParams { thread_id: idle_child_id.to_string(), + turn_id: "turn".into(), + call_id: "call".into(), + namespace: None, + tool: "test".into(), + arguments: serde_json::Value::Null, }, })), ) diff --git a/codex-rs/tui/src/app/app_server_event_targets.rs b/codex-rs/tui/src/app/app_server_event_targets.rs index 3fc59064a4..69432801c0 100644 --- a/codex-rs/tui/src/app/app_server_event_targets.rs +++ b/codex-rs/tui/src/app/app_server_event_targets.rs @@ -24,9 +24,6 @@ pub(super) fn server_request_thread_id(request: &ServerRequest) -> Option { ThreadId::from_string(¶ms.thread_id).ok() } - ServerRequest::CurrentTimeRead { params, .. } => { - ThreadId::from_string(¶ms.thread_id).ok() - } ServerRequest::ChatgptAuthTokensRefresh { .. } | ServerRequest::AttestationGenerate { .. } | ServerRequest::ApplyPatchApproval { .. } diff --git a/codex-rs/tui/src/app/app_server_events.rs b/codex-rs/tui/src/app/app_server_events.rs index c931e3f2c6..cccac3bbe6 100644 --- a/codex-rs/tui/src/app/app_server_events.rs +++ b/codex-rs/tui/src/app/app_server_events.rs @@ -791,7 +791,6 @@ impl App { &request, ServerRequest::DynamicToolCall { .. } | ServerRequest::AttestationGenerate { .. } - | ServerRequest::CurrentTimeRead { .. } | ServerRequest::ApplyPatchApproval { .. } | ServerRequest::ExecCommandApproval { .. } ); diff --git a/codex-rs/tui/src/app/app_server_requests.rs b/codex-rs/tui/src/app/app_server_requests.rs index c31fdc6eb3..0ee0dd2d29 100644 --- a/codex-rs/tui/src/app/app_server_requests.rs +++ b/codex-rs/tui/src/app/app_server_requests.rs @@ -203,12 +203,6 @@ impl PendingAppServerRequests { message: "Attestation generation is not available in TUI.".to_string(), }) } - ServerRequest::CurrentTimeRead { request_id, .. } => { - Some(UnsupportedAppServerRequest { - request_id: request_id.clone(), - message: "External current time is not available in TUI.".to_string(), - }) - } ServerRequest::ApplyPatchApproval { request_id, .. } => { Some(UnsupportedAppServerRequest { request_id: request_id.clone(), @@ -530,7 +524,6 @@ impl PendingAppServerRequests { ServerRequest::ChatgptAuthTokensRefresh { .. } => None, ServerRequest::DynamicToolCall { .. } | ServerRequest::AttestationGenerate { .. } - | ServerRequest::CurrentTimeRead { .. } | ServerRequest::ApplyPatchApproval { .. } | ServerRequest::ExecCommandApproval { .. } => None, } diff --git a/codex-rs/tui/src/app/side.rs b/codex-rs/tui/src/app/side.rs index fd8c787a14..1b47ccf682 100644 --- a/codex-rs/tui/src/app/side.rs +++ b/codex-rs/tui/src/app/side.rs @@ -67,7 +67,6 @@ impl SideParentStatus { | ServerRequest::ExecCommandApproval { .. } => Some(SideParentStatus::NeedsApproval), ServerRequest::DynamicToolCall { .. } | ServerRequest::AttestationGenerate { .. } - | ServerRequest::CurrentTimeRead { .. } | ServerRequest::ChatgptAuthTokensRefresh { .. } => None, } } diff --git a/codex-rs/tui/src/chatwidget/protocol_requests.rs b/codex-rs/tui/src/chatwidget/protocol_requests.rs index e76b50b6ee..8a4db4964a 100644 --- a/codex-rs/tui/src/chatwidget/protocol_requests.rs +++ b/codex-rs/tui/src/chatwidget/protocol_requests.rs @@ -50,7 +50,6 @@ impl ChatWidget { } ServerRequest::DynamicToolCall { .. } | ServerRequest::AttestationGenerate { .. } - | ServerRequest::CurrentTimeRead { .. } | ServerRequest::ChatgptAuthTokensRefresh { .. } | ServerRequest::ApplyPatchApproval { .. } | ServerRequest::ExecCommandApproval { .. } => {