From d1d0a7485bd2b3d186e3a06c6ff3423417b5c063 Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Wed, 17 Jun 2026 19:37:27 -0700 Subject: [PATCH 01/15] Add app-server current-time provider --- .../analytics/src/analytics_client_tests.rs | 4 + codex-rs/app-server-client/src/lib.rs | 1 + codex-rs/app-server-client/src/remote.rs | 1 + .../schema/json/ClientRequest.json | 5 + .../schema/json/CurrentTimeReadParams.json | 13 ++ .../schema/json/CurrentTimeReadResponse.json | 15 ++ .../schema/json/ServerRequest.json | 36 ++++ .../codex_app_server_protocol.schemas.json | 58 +++++++ .../codex_app_server_protocol.v2.schemas.json | 5 + .../schema/json/v1/InitializeParams.json | 5 + .../typescript/InitializeCapabilities.ts | 4 + .../schema/typescript/ServerRequest.ts | 3 +- .../typescript/v2/CurrentTimeReadParams.ts | 5 + .../typescript/v2/CurrentTimeReadResponse.ts | 9 + .../schema/typescript/v2/index.ts | 2 + .../src/protocol/common.rs | 9 + .../app-server-protocol/src/protocol/v1.rs | 3 + .../src/protocol/v2/current_time.rs | 19 +++ .../src/protocol/v2/mod.rs | 2 + codex-rs/app-server-test-client/src/lib.rs | 1 + codex-rs/app-server/src/current_time.rs | 95 +++++++++++ codex-rs/app-server/src/lib.rs | 4 + codex-rs/app-server/src/message_processor.rs | 16 +- .../initialize_processor.rs | 2 + .../thread_processor_tests.rs | 3 + codex-rs/app-server/src/thread_state.rs | 29 +++- .../app-server/tests/suite/v2/attestation.rs | 1 + .../app-server/tests/suite/v2/current_time.rs | 157 ++++++++++++++++++ .../tests/suite/v2/experimental_api.rs | 8 + .../app-server/tests/suite/v2/initialize.rs | 1 + codex-rs/app-server/tests/suite/v2/mod.rs | 1 + .../tests/suite/v2/thread_status.rs | 1 + 32 files changed, 511 insertions(+), 7 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/json/CurrentTimeReadParams.json create mode 100644 codex-rs/app-server-protocol/schema/json/CurrentTimeReadResponse.json create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/CurrentTimeReadParams.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/CurrentTimeReadResponse.ts create mode 100644 codex-rs/app-server-protocol/src/protocol/v2/current_time.rs create mode 100644 codex-rs/app-server/src/current_time.rs create mode 100644 codex-rs/app-server/tests/suite/v2/current_time.rs diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index fcdd97af8ec1..2e687b85059b 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -773,6 +773,7 @@ fn sample_initialize_fact(connection_id: u64) -> AnalyticsFact { capabilities: Some(InitializeCapabilities { experimental_api: false, request_attestation: false, + request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), @@ -1669,6 +1670,7 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize capabilities: Some(InitializeCapabilities { experimental_api: false, request_attestation: false, + request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), @@ -1819,6 +1821,7 @@ async fn compaction_event_ingests_custom_fact() { capabilities: Some(InitializeCapabilities { experimental_api: false, request_attestation: false, + request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), @@ -1948,6 +1951,7 @@ async fn guardian_review_event_ingests_custom_fact_with_optional_target_item() { capabilities: Some(InitializeCapabilities { experimental_api: false, request_attestation: false, + request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index b7a958eff1f5..faecbc9bf2a4 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -371,6 +371,7 @@ impl InProcessClientStartArgs { let capabilities = InitializeCapabilities { experimental_api: self.experimental_api, request_attestation: false, + request_current_time: false, opt_out_notification_methods: if self.opt_out_notification_methods.is_empty() { None } else { diff --git a/codex-rs/app-server-client/src/remote.rs b/codex-rs/app-server-client/src/remote.rs index 5575fe4ad46a..cda250feb291 100644 --- a/codex-rs/app-server-client/src/remote.rs +++ b/codex-rs/app-server-client/src/remote.rs @@ -95,6 +95,7 @@ impl RemoteAppServerConnectArgs { let capabilities = InitializeCapabilities { experimental_api: self.experimental_api, request_attestation: false, + request_current_time: false, opt_out_notification_methods: if self.opt_out_notification_methods.is_empty() { None } else { diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index e183c88b08af..d045e87154ab 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1290,6 +1290,11 @@ "default": false, "description": "Opt into `attestation/generate` requests for upstream `x-oai-attestation`.", "type": "boolean" + }, + "requestCurrentTime": { + "default": false, + "description": "Opt into `currentTime/read` requests for an external clock.", + "type": "boolean" } }, "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/CurrentTimeReadParams.json b/codex-rs/app-server-protocol/schema/json/CurrentTimeReadParams.json new file mode 100644 index 000000000000..f86754493cb5 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/CurrentTimeReadParams.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "CurrentTimeReadParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/CurrentTimeReadResponse.json b/codex-rs/app-server-protocol/schema/json/CurrentTimeReadResponse.json new file mode 100644 index 000000000000..592cd6188b7b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/CurrentTimeReadResponse.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "currentTimeAt": { + "description": "Current time as whole Unix seconds.", + "format": "int64", + "type": "integer" + } + }, + "required": [ + "currentTimeAt" + ], + "title": "CurrentTimeReadResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index c6ac80dee514..e5e4a0724c95 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -448,6 +448,17 @@ ], "type": "object" }, + "CurrentTimeReadParams": { + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, "DynamicToolCallParams": { "properties": { "arguments": true, @@ -1994,6 +2005,31 @@ "title": "Attestation/generateRequest", "type": "object" }, + { + "description": "Read the current time from an external clock owned by the client.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "currentTime/read" + ], + "title": "CurrentTime/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/CurrentTimeReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "CurrentTime/readRequest", + "type": "object" + }, { "description": "DEPRECATED APIs below Request to approve a patch. This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 8ef80b7b542a..d1536de614ac 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -2469,6 +2469,34 @@ "title": "CommandExecutionRequestApprovalResponse", "type": "object" }, + "CurrentTimeReadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "CurrentTimeReadParams", + "type": "object" + }, + "CurrentTimeReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "currentTimeAt": { + "description": "Current time as whole Unix seconds.", + "format": "int64", + "type": "integer" + } + }, + "required": [ + "currentTimeAt" + ], + "title": "CurrentTimeReadResponse", + "type": "object" + }, "DynamicToolCallParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -2918,6 +2946,11 @@ "default": false, "description": "Opt into `attestation/generate` requests for upstream `x-oai-attestation`.", "type": "boolean" + }, + "requestCurrentTime": { + "default": false, + "description": "Opt into `currentTime/read` requests for an external clock.", + "type": "boolean" } }, "type": "object" @@ -5638,6 +5671,31 @@ "title": "Attestation/generateRequest", "type": "object" }, + { + "description": "Read the current time from an external clock owned by the client.", + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "currentTime/read" + ], + "title": "CurrentTime/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/CurrentTimeReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "CurrentTime/readRequest", + "type": "object" + }, { "description": "DEPRECATED APIs below Request to approve a patch. This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 00d7a4bbdfac..cfc577b2752d 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -7458,6 +7458,11 @@ "default": false, "description": "Opt into `attestation/generate` requests for upstream `x-oai-attestation`.", "type": "boolean" + }, + "requestCurrentTime": { + "default": false, + "description": "Opt into `currentTime/read` requests for an external clock.", + "type": "boolean" } }, "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json b/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json index 75f0860ddda5..f672bdcae114 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json @@ -48,6 +48,11 @@ "default": false, "description": "Opt into `attestation/generate` requests for upstream `x-oai-attestation`.", "type": "boolean" + }, + "requestCurrentTime": { + "default": false, + "description": "Opt into `currentTime/read` requests for an external clock.", + "type": "boolean" } }, "type": "object" diff --git a/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts b/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts index dcc4dffb0b51..e9f4073f6957 100644 --- a/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts +++ b/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts @@ -18,6 +18,10 @@ requestAttestation: boolean, * Allow downstream MCP servers to request OpenAI extended form elicitations. */ mcpServerOpenaiFormElicitation?: boolean, +/** + * Opt into `currentTime/read` requests for an external clock. + */ +requestCurrentTime: boolean, /** * Exact notification method names that should be suppressed for this * connection (for example `thread/started`). diff --git a/codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts index 80e9ffc1162c..a6eaccb77a1d 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts @@ -7,6 +7,7 @@ import type { RequestId } from "./RequestId"; import type { AttestationGenerateParams } from "./v2/AttestationGenerateParams"; import type { ChatgptAuthTokensRefreshParams } from "./v2/ChatgptAuthTokensRefreshParams"; import type { CommandExecutionRequestApprovalParams } from "./v2/CommandExecutionRequestApprovalParams"; +import type { CurrentTimeReadParams } from "./v2/CurrentTimeReadParams"; import type { DynamicToolCallParams } from "./v2/DynamicToolCallParams"; import type { FileChangeRequestApprovalParams } from "./v2/FileChangeRequestApprovalParams"; import type { McpServerElicitationRequestParams } from "./v2/McpServerElicitationRequestParams"; @@ -16,4 +17,4 @@ import type { ToolRequestUserInputParams } from "./v2/ToolRequestUserInputParams /** * Request initiated from the server and sent to the client. */ -export type ServerRequest = { "method": "item/commandExecution/requestApproval", id: RequestId, params: CommandExecutionRequestApprovalParams, } | { "method": "item/fileChange/requestApproval", id: RequestId, params: FileChangeRequestApprovalParams, } | { "method": "item/tool/requestUserInput", id: RequestId, params: ToolRequestUserInputParams, } | { "method": "mcpServer/elicitation/request", id: RequestId, params: McpServerElicitationRequestParams, } | { "method": "item/permissions/requestApproval", id: RequestId, params: PermissionsRequestApprovalParams, } | { "method": "item/tool/call", id: RequestId, params: DynamicToolCallParams, } | { "method": "account/chatgptAuthTokens/refresh", id: RequestId, params: ChatgptAuthTokensRefreshParams, } | { "method": "attestation/generate", id: RequestId, params: AttestationGenerateParams, } | { "method": "applyPatchApproval", id: RequestId, params: ApplyPatchApprovalParams, } | { "method": "execCommandApproval", id: RequestId, params: ExecCommandApprovalParams, }; +export type ServerRequest = { "method": "item/commandExecution/requestApproval", id: RequestId, params: CommandExecutionRequestApprovalParams, } | { "method": "item/fileChange/requestApproval", id: RequestId, params: FileChangeRequestApprovalParams, } | { "method": "item/tool/requestUserInput", id: RequestId, params: ToolRequestUserInputParams, } | { "method": "mcpServer/elicitation/request", id: RequestId, params: McpServerElicitationRequestParams, } | { "method": "item/permissions/requestApproval", id: RequestId, params: PermissionsRequestApprovalParams, } | { "method": "item/tool/call", id: RequestId, params: DynamicToolCallParams, } | { "method": "account/chatgptAuthTokens/refresh", id: RequestId, params: ChatgptAuthTokensRefreshParams, } | { "method": "attestation/generate", id: RequestId, params: AttestationGenerateParams, } | { "method": "currentTime/read", id: RequestId, params: CurrentTimeReadParams, } | { "method": "applyPatchApproval", id: RequestId, params: ApplyPatchApprovalParams, } | { "method": "execCommandApproval", id: RequestId, params: ExecCommandApprovalParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CurrentTimeReadParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CurrentTimeReadParams.ts new file mode 100644 index 000000000000..80a3e3038731 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CurrentTimeReadParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CurrentTimeReadParams = { threadId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CurrentTimeReadResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CurrentTimeReadResponse.ts new file mode 100644 index 000000000000..101400a25c0e --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CurrentTimeReadResponse.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CurrentTimeReadResponse = { +/** + * Current time as whole Unix seconds. + */ +currentTimeAt: bigint, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 2e59b0fe1908..c9b86608f008 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -90,6 +90,8 @@ export type { ConsumeAccountRateLimitResetCreditParams } from "./ConsumeAccountR export type { ConsumeAccountRateLimitResetCreditResponse } from "./ConsumeAccountRateLimitResetCreditResponse"; export type { ContextCompactedNotification } from "./ContextCompactedNotification"; export type { CreditsSnapshot } from "./CreditsSnapshot"; +export type { CurrentTimeReadParams } from "./CurrentTimeReadParams"; +export type { CurrentTimeReadResponse } from "./CurrentTimeReadResponse"; export type { DeprecationNoticeNotification } from "./DeprecationNoticeNotification"; export type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; export type { DynamicToolCallParams } from "./DynamicToolCallParams"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index c1fac138a867..056d439d6caa 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1470,6 +1470,12 @@ server_request_definitions! { response: v2::AttestationGenerateResponse, }, + /// 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). @@ -2163,6 +2169,7 @@ mod tests { experimental_api: true, request_attestation: true, mcp_server_openai_form_elicitation: true, + request_current_time: false, opt_out_notification_methods: Some(vec![ "thread/started".to_string(), "item/agentMessage/delta".to_string(), @@ -2185,6 +2192,7 @@ mod tests { "experimentalApi": true, "requestAttestation": true, "mcpServerOpenaiFormElicitation": true, + "requestCurrentTime": false, "optOutNotificationMethods": [ "thread/started", "item/agentMessage/delta" @@ -2234,6 +2242,7 @@ mod tests { experimental_api: true, request_attestation: true, mcp_server_openai_form_elicitation: true, + request_current_time: false, opt_out_notification_methods: Some(vec![ "thread/started".to_string(), "item/agentMessage/delta".to_string(), diff --git a/codex-rs/app-server-protocol/src/protocol/v1.rs b/codex-rs/app-server-protocol/src/protocol/v1.rs index dccea51a3601..7e74411131eb 100644 --- a/codex-rs/app-server-protocol/src/protocol/v1.rs +++ b/codex-rs/app-server-protocol/src/protocol/v1.rs @@ -53,6 +53,9 @@ pub struct InitializeCapabilities { /// Allow downstream MCP servers to request OpenAI extended form elicitations. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub mcp_server_openai_form_elicitation: bool, + /// Opt into `currentTime/read` requests for an external clock. + #[serde(default)] + pub request_current_time: bool, /// Exact notification method names that should be suppressed for this /// connection (for example `thread/started`). #[ts(optional = nullable)] 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 new file mode 100644 index 000000000000..aad197f29cf6 --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/current_time.rs @@ -0,0 +1,19 @@ +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use ts_rs::TS; + +#[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. + 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 b5fa9fdc6590..1097b038116e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/mod.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/mod.rs @@ -6,6 +6,7 @@ mod attestation; mod collaboration_mode; mod command_exec; mod config; +mod current_time; mod environment; mod experimental_feature; mod feedback; @@ -32,6 +33,7 @@ pub use attestation::*; pub use collaboration_mode::*; pub use command_exec::*; pub use config::*; +pub use current_time::*; pub use environment::*; pub use experimental_feature::*; pub use feedback::*; diff --git a/codex-rs/app-server-test-client/src/lib.rs b/codex-rs/app-server-test-client/src/lib.rs index 856d75585ec3..d17397e3857c 100644 --- a/codex-rs/app-server-test-client/src/lib.rs +++ b/codex-rs/app-server-test-client/src/lib.rs @@ -1662,6 +1662,7 @@ impl CodexClient { capabilities: Some(InitializeCapabilities { experimental_api, request_attestation: false, + request_current_time: false, opt_out_notification_methods: Some( NOTIFICATIONS_TO_OPT_OUT .iter() diff --git a/codex-rs/app-server/src/current_time.rs b/codex-rs/app-server/src/current_time.rs new file mode 100644 index 000000000000..04386cc8ebd3 --- /dev/null +++ b/codex-rs/app-server/src/current_time.rs @@ -0,0 +1,95 @@ +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::CurrentTimeFuture; +use codex_core::CurrentTimeProvider; +use codex_protocol::ThreadId; +use tokio::time::Duration; +use tokio::time::timeout; + +use crate::outgoing_message::OutgoingMessageSender; +use crate::thread_state::ThreadStateManager; + +const CURRENT_TIME_READ_TIMEOUT: Duration = Duration::from_secs(5); + +pub(crate) fn app_server_current_time_provider( + outgoing: Arc, + thread_state_manager: ThreadStateManager, +) -> Arc { + Arc::new(AppServerCurrentTimeProvider { + outgoing: Arc::downgrade(&outgoing), + thread_state_manager, + }) +} + +struct AppServerCurrentTimeProvider { + outgoing: Weak, + thread_state_manager: ThreadStateManager, +} + +impl CurrentTimeProvider for AppServerCurrentTimeProvider { + fn current_time(&self, thread_id: ThreadId) -> CurrentTimeFuture<'_> { + 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 + }) + } +} + +async fn request_current_time( + outgoing: Arc, + thread_state_manager: ThreadStateManager, + thread_id: ThreadId, +) -> Result> { + let connection_id = thread_state_manager + .first_current_time_capable_connection_for_thread(thread_id) + .await + .ok_or_else(|| anyhow!("no current-time capable client is subscribed to the thread"))?; + 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(), + }), + Some(thread_id), + ) + .await; + + let result = match timeout(CURRENT_TIME_READ_TIMEOUT, 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_READ_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")) +} diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index cac2db54ebb3..f66954950777 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -91,6 +91,7 @@ mod config_manager; mod config_manager_service; mod connection_cleanup; mod connection_rpc_gate; +mod current_time; mod dynamic_tools; mod error_code; mod extensions; @@ -1068,6 +1069,9 @@ pub async fn run_main_with_transport_options( connection_state .session .request_attestation(), + connection_state + .session + .request_current_time(), ) .await; connection_state diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 0f9f6d3321fa..d51fefa8f6a9 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -7,6 +7,7 @@ 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_current_time_provider; use crate::error_code::invalid_request; use crate::extensions::ThreadExtensionDependencies; use crate::extensions::app_server_extension_event_sink; @@ -224,6 +225,7 @@ pub(crate) struct InitializedConnectionSessionState { pub(crate) client_version: String, pub(crate) request_attestation: bool, pub(crate) supports_openai_form_elicitation: bool, + pub(crate) request_current_time: bool, } impl Default for ConnectionSessionState { @@ -281,6 +283,12 @@ impl ConnectionSessionState { .is_some_and(|session| session.supports_openai_form_elicitation) } + pub(crate) fn request_current_time(&self) -> bool { + self.initialized + .get() + .is_some_and(|session| session.request_current_time) + } + pub(crate) fn initialize(&self, session: InitializedConnectionSessionState) -> Result<(), ()> { self.initialized.set(session).map_err(|_| ()) } @@ -379,7 +387,10 @@ impl MessageProcessor { outgoing.clone(), thread_state_manager.clone(), )), - /*external_time_provider*/ None, + Some(app_server_current_time_provider( + outgoing.clone(), + thread_state_manager.clone(), + )), ) }); let models_manager = thread_manager.get_models_manager(); @@ -730,12 +741,14 @@ impl MessageProcessor { &self, connection_id: ConnectionId, request_attestation: bool, + request_current_time: bool, ) { self.thread_processor .connection_initialized( connection_id, ConnectionCapabilities { request_attestation, + request_current_time, }, ) .await; @@ -850,6 +863,7 @@ impl MessageProcessor { connection_id, ConnectionCapabilities { request_attestation: session.request_attestation(), + request_current_time: session.request_current_time(), }, ) .await; diff --git a/codex-rs/app-server/src/request_processors/initialize_processor.rs b/codex-rs/app-server/src/request_processors/initialize_processor.rs index cfdad27f50ff..bc0497f65df3 100644 --- a/codex-rs/app-server/src/request_processors/initialize_processor.rs +++ b/codex-rs/app-server/src/request_processors/initialize_processor.rs @@ -71,6 +71,7 @@ impl InitializeRequestProcessor { let experimental_api_enabled = capabilities.experimental_api; let request_attestation = capabilities.request_attestation; let supports_openai_form_elicitation = capabilities.mcp_server_openai_form_elicitation; + let request_current_time = capabilities.request_current_time; let opt_out_notification_methods = capabilities .opt_out_notification_methods .unwrap_or_default(); @@ -98,6 +99,7 @@ impl InitializeRequestProcessor { client_version: version, request_attestation, supports_openai_form_elicitation, + request_current_time, }) .is_err() { diff --git a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs index 1f333f1eb179..65b409713754 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs @@ -1421,6 +1421,7 @@ mod thread_processor_behavior_tests { unrelated_supported_connection, ConnectionCapabilities { request_attestation: true, + ..Default::default() }, ) .await; @@ -1429,6 +1430,7 @@ mod thread_processor_behavior_tests { earlier_supported_connection, ConnectionCapabilities { request_attestation: true, + ..Default::default() }, ) .await; @@ -1437,6 +1439,7 @@ mod thread_processor_behavior_tests { later_supported_connection, ConnectionCapabilities { request_attestation: true, + ..Default::default() }, ) .await; diff --git a/codex-rs/app-server/src/thread_state.rs b/codex-rs/app-server/src/thread_state.rs index 6d2b48a4c88b..bc2c3d6fbe28 100644 --- a/codex-rs/app-server/src/thread_state.rs +++ b/codex-rs/app-server/src/thread_state.rs @@ -281,6 +281,7 @@ struct ThreadStateManagerInner { #[derive(Clone, Copy, Default)] pub(crate) struct ConnectionCapabilities { pub(crate) request_attestation: bool, + pub(crate) request_current_time: bool, } #[derive(Clone, Default)] @@ -312,6 +313,27 @@ impl ThreadStateManager { pub(crate) async fn first_attestation_capable_connection_for_thread( &self, thread_id: ThreadId, + ) -> Option { + self.first_capable_connection_for_thread(thread_id, |capabilities| { + capabilities.request_attestation + }) + .await + } + + pub(crate) async fn first_current_time_capable_connection_for_thread( + &self, + thread_id: ThreadId, + ) -> Option { + self.first_capable_connection_for_thread(thread_id, |capabilities| { + capabilities.request_current_time + }) + .await + } + + async fn first_capable_connection_for_thread( + &self, + thread_id: ThreadId, + is_capable: impl Fn(&ConnectionCapabilities) -> bool, ) -> Option { let state = self.state.lock().await; state @@ -320,11 +342,8 @@ impl ThreadStateManager { .connection_ids .iter() .filter_map(|connection_id| { - state - .live_connections - .get(connection_id)? - .request_attestation - .then_some(*connection_id) + let capabilities = state.live_connections.get(connection_id)?; + is_capable(capabilities).then_some(*connection_id) }) .min_by_key(|connection_id| connection_id.0) } diff --git a/codex-rs/app-server/tests/suite/v2/attestation.rs b/codex-rs/app-server/tests/suite/v2/attestation.rs index ea567a2d43cb..6c4458f4a1fc 100644 --- a/codex-rs/app-server/tests/suite/v2/attestation.rs +++ b/codex-rs/app-server/tests/suite/v2/attestation.rs @@ -80,6 +80,7 @@ async fn attestation_generate_round_trip_adds_header_to_responses_websocket_hand Some(InitializeCapabilities { experimental_api: true, request_attestation: true, + request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), diff --git a/codex-rs/app-server/tests/suite/v2/current_time.rs b/codex-rs/app-server/tests/suite/v2/current_time.rs new file mode 100644 index 000000000000..c5cf0e5de3d6 --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/current_time.rs @@ -0,0 +1,157 @@ +use std::path::Path; + +use anyhow::Result; +use anyhow::bail; +use app_test_support::TestAppServer; +use app_test_support::to_response; +use codex_app_server_protocol::ClientInfo; +use codex_app_server_protocol::CurrentTimeReadResponse; +use codex_app_server_protocol::InitializeCapabilities; +use codex_app_server_protocol::JSONRPCMessage; +use codex_app_server_protocol::JSONRPCResponse; +use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::ServerRequest; +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<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let response_mock = responses::mount_sse_once( + &server, + responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_assistant_message("msg-1", "Done"), + responses::ev_completed("resp-1"), + ]), + ) + .await; + let codex_home = TempDir::new()?; + write_config(codex_home.path(), &server.uri())?; + + let mut app_server = TestAppServer::new(codex_home.path()).await?; + timeout( + DEFAULT_READ_TIMEOUT, + app_server.initialize_with_capabilities( + ClientInfo { + name: "codex-app-server-tests".to_string(), + title: None, + version: "0.1.0".to_string(), + }, + Some(InitializeCapabilities { + experimental_api: true, + request_current_time: true, + ..Default::default() + }), + ), + ) + .await??; + + let thread_request_id = app_server + .send_thread_start_request(ThreadStartParams::default()) + .await?; + let thread_response: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + app_server.read_stream_until_response_message(RequestId::Integer(thread_request_id)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response(thread_response)?; + + let turn_request_id = app_server + .send_turn_start_request(TurnStartParams { + thread_id: thread.id.clone(), + input: vec![UserInput::Text { + text: "What time is it?".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + let turn_response: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + app_server.read_stream_until_response_message(RequestId::Integer(turn_request_id)), + ) + .await??; + let _: TurnStartResponse = to_response(turn_response)?; + + timeout(DEFAULT_READ_TIMEOUT, async { + loop { + match app_server.read_next_message().await? { + JSONRPCMessage::Request(request) => { + let request = ServerRequest::try_from(request)?; + let ServerRequest::CurrentTimeRead { request_id, params } = request else { + bail!("expected currentTime/read request, got {request:?}"); + }; + assert_eq!(params.thread_id, thread.id); + 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(()); + } + _ => {} + } + } + }) + .await??; + + assert!( + response_mock + .single_request() + .message_input_texts("developer") + .contains(&CURRENT_TIME_REMINDER.to_string()) + ); + Ok(()) +} + +fn write_config(codex_home: &Path, server_uri: &str) -> std::io::Result<()> { + std::fs::write( + codex_home.join("config.toml"), + format!( + r#" +model = "mock-model" +approval_policy = "never" +sandbox_mode = "read-only" +model_provider = "mock_provider" + +[features.current_time_reminder] +enabled = true +reminder_interval_model_requests = 1 +clock_source = "app_server_client" + +[model_providers.mock_provider] +name = "Mock provider for test" +base_url = "{server_uri}/v1" +wire_api = "responses" +request_max_retries = 0 +stream_max_retries = 0 +"# + ), + ) +} diff --git a/codex-rs/app-server/tests/suite/v2/experimental_api.rs b/codex-rs/app-server/tests/suite/v2/experimental_api.rs index 62ad4c067784..83b27fb969b9 100644 --- a/codex-rs/app-server/tests/suite/v2/experimental_api.rs +++ b/codex-rs/app-server/tests/suite/v2/experimental_api.rs @@ -38,6 +38,7 @@ async fn mock_experimental_method_requires_experimental_api_capability() -> Resu Some(InitializeCapabilities { experimental_api: false, request_attestation: false, + request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), @@ -70,6 +71,7 @@ async fn realtime_conversation_start_requires_experimental_api_capability() -> R Some(InitializeCapabilities { experimental_api: false, request_attestation: false, + request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), @@ -117,6 +119,7 @@ async fn thread_memory_mode_set_requires_experimental_api_capability() -> Result Some(InitializeCapabilities { experimental_api: false, request_attestation: false, + request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), @@ -152,6 +155,7 @@ async fn thread_settings_update_requires_experimental_api_capability() -> Result Some(InitializeCapabilities { experimental_api: false, request_attestation: false, + request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), @@ -187,6 +191,7 @@ async fn realtime_webrtc_start_requires_experimental_api_capability() -> Result< Some(InitializeCapabilities { experimental_api: false, request_attestation: false, + request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), @@ -238,6 +243,7 @@ async fn thread_start_mock_field_requires_experimental_api_capability() -> Resul Some(InitializeCapabilities { experimental_api: false, request_attestation: false, + request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), @@ -277,6 +283,7 @@ async fn thread_start_without_dynamic_tools_allows_without_experimental_api_capa Some(InitializeCapabilities { experimental_api: false, request_attestation: false, + request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), @@ -315,6 +322,7 @@ async fn thread_start_granular_approval_policy_requires_experimental_api_capabil Some(InitializeCapabilities { experimental_api: false, request_attestation: false, + request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), diff --git a/codex-rs/app-server/tests/suite/v2/initialize.rs b/codex-rs/app-server/tests/suite/v2/initialize.rs index 6fb7aa3bc314..8c76c4dbbce4 100644 --- a/codex-rs/app-server/tests/suite/v2/initialize.rs +++ b/codex-rs/app-server/tests/suite/v2/initialize.rs @@ -213,6 +213,7 @@ async fn initialize_opt_out_notification_methods_filters_notifications() -> Resu Some(InitializeCapabilities { experimental_api: true, request_attestation: false, + request_current_time: false, opt_out_notification_methods: Some(vec!["thread/started".to_string()]), mcp_server_openai_form_elicitation: false, }), diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index 9d07da06600a..2745e8ee004c 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -11,6 +11,7 @@ mod config_rpc; mod connection_handling_websocket; #[cfg(unix)] mod connection_handling_websocket_unix; +mod current_time; mod dynamic_tools; #[cfg(not(target_os = "windows"))] mod executor_mcp; diff --git a/codex-rs/app-server/tests/suite/v2/thread_status.rs b/codex-rs/app-server/tests/suite/v2/thread_status.rs index e922bd13c14e..f646d7bced2e 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_status.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_status.rs @@ -147,6 +147,7 @@ async fn thread_status_changed_can_be_opted_out() -> Result<()> { Some(InitializeCapabilities { experimental_api: true, request_attestation: false, + request_current_time: false, opt_out_notification_methods: Some(vec!["thread/status/changed".to_string()]), mcp_server_openai_form_elicitation: false, }), From a982aa7c1b7b002ebba129bd8dcaec4a097fbf62 Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Wed, 17 Jun 2026 19:44:59 -0700 Subject: [PATCH 02/15] Align current-time tests with app-server precedent --- .../src/protocol/common.rs | 26 +++++++ .../app-server/tests/suite/v2/current_time.rs | 71 +++++++++---------- 2 files changed, 59 insertions(+), 38 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 056d439d6caa..90cf8486ce9c 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -2381,6 +2381,32 @@ 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/tests/suite/v2/current_time.rs b/codex-rs/app-server/tests/suite/v2/current_time.rs index c5cf0e5de3d6..4bddd9597d93 100644 --- a/codex-rs/app-server/tests/suite/v2/current_time.rs +++ b/codex-rs/app-server/tests/suite/v2/current_time.rs @@ -1,8 +1,9 @@ use std::path::Path; use anyhow::Result; -use anyhow::bail; +use app_test_support::DEFAULT_CLIENT_NAME; use app_test_support::TestAppServer; +use app_test_support::create_final_assistant_message_sse_response; use app_test_support::to_response; use codex_app_server_protocol::ClientInfo; use codex_app_server_protocol::CurrentTimeReadResponse; @@ -37,22 +38,18 @@ async fn current_time_read_round_trip_adds_reminder_to_model_input() -> Result<( let server = responses::start_mock_server().await; let response_mock = responses::mount_sse_once( &server, - responses::sse(vec![ - responses::ev_response_created("resp-1"), - responses::ev_assistant_message("msg-1", "Done"), - responses::ev_completed("resp-1"), - ]), + create_final_assistant_message_sse_response("Done")?, ) .await; let codex_home = TempDir::new()?; - write_config(codex_home.path(), &server.uri())?; + create_config_toml(codex_home.path(), &server.uri())?; let mut app_server = TestAppServer::new(codex_home.path()).await?; - timeout( + let initialized = timeout( DEFAULT_READ_TIMEOUT, app_server.initialize_with_capabilities( ClientInfo { - name: "codex-app-server-tests".to_string(), + name: DEFAULT_CLIENT_NAME.to_string(), title: None, version: "0.1.0".to_string(), }, @@ -64,6 +61,9 @@ async fn current_time_read_round_trip_adds_reminder_to_model_input() -> Result<( ), ) .await??; + let JSONRPCMessage::Response(_) = initialized else { + panic!("expected initialize response, got: {initialized:?}"); + }; let thread_request_id = app_server .send_thread_start_request(ThreadStartParams::default()) @@ -92,45 +92,40 @@ async fn current_time_read_round_trip_adds_reminder_to_model_input() -> Result<( .await??; let _: TurnStartResponse = to_response(turn_response)?; - timeout(DEFAULT_READ_TIMEOUT, async { - loop { - match app_server.read_next_message().await? { - JSONRPCMessage::Request(request) => { - let request = ServerRequest::try_from(request)?; - let ServerRequest::CurrentTimeRead { request_id, params } = request else { - bail!("expected currentTime/read request, got {request:?}"); - }; - assert_eq!(params.thread_id, thread.id); - 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(()); - } - _ => {} - } - } - }) + let server_request = timeout( + DEFAULT_READ_TIMEOUT, + app_server.read_stream_until_request_message(), + ) + .await??; + let ServerRequest::CurrentTimeRead { request_id, params } = server_request else { + panic!("expected CurrentTimeRead request, got: {server_request:?}"); + }; + assert_eq!(params.thread_id, thread.id); + app_server + .send_response( + request_id, + serde_json::to_value(CurrentTimeReadResponse { + current_time_at: CURRENT_TIME_AT, + })?, + ) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + app_server.read_stream_until_notification_message("turn/completed"), + ) .await??; assert!( response_mock .single_request() .message_input_texts("developer") - .contains(&CURRENT_TIME_REMINDER.to_string()) + .iter() + .any(|text| text == CURRENT_TIME_REMINDER) ); Ok(()) } -fn write_config(codex_home: &Path, server_uri: &str) -> std::io::Result<()> { +fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> { std::fs::write( codex_home.join("config.toml"), format!( From 77afe0d9b1b70d4f61185105172e89affde36640 Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Wed, 17 Jun 2026 19:52:38 -0700 Subject: [PATCH 03/15] Document current-time app-server request --- codex-rs/app-server/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 183c7c6007b1..5672245d60a7 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -1469,6 +1469,10 @@ When the client responds to `item/tool/requestUserInput`, the server emits `serv Desktop hosts that provide upstream attestation should set `capabilities.requestAttestation` during `initialize` and handle the server-initiated `attestation/generate` request. App-server issues it just in time before ChatGPT Codex requests that forward `x-oai-attestation`; the client responds with `{ "token": "v1." }`, where `token` is an opaque client-owned value. When app-server receives a client response, it forwards a consistent outer envelope such as `{ "v": 1, "s": 0, "t": "v1." }`, where `t` contains the client token unchanged. If app-server attempts attestation but fails within its own boundary, it sends the same envelope shape with an app-server status code and without `t` (`1 = timeout`, `2 = request failed`, `3 = request canceled`, `4 = malformed response`). If no initialized client opted into attestation, app-server omits `x-oai-attestation` for that upstream request. +### Current time + +Hosts that provide an external clock should set `capabilities.requestCurrentTime` during `initialize` and handle the server-initiated `currentTime/read` request. When `[features.current_time_reminder]` is enabled with `clock_source = "app_server_client"`, app-server sends `{ "threadId": "thr_123" }` when a time reminder is due. The client responds with `{ "currentTimeAt": 1781717655 }`, where `currentTimeAt` is an integer Unix timestamp in seconds. A failed, canceled, timed-out, or malformed response stops the turn before the model request is sent. + ### MCP server elicitations MCP servers can interrupt a turn and ask the client for structured input via `mcpServer/elicitation/request`. From fa9aa3d563be6e69a8afc0ef55861cb88c6708a1 Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Wed, 17 Jun 2026 20:19:56 -0700 Subject: [PATCH 04/15] Reject ambiguous current-time providers --- codex-rs/app-server/src/current_time.rs | 46 +++++++++++++++++++++-- codex-rs/app-server/src/thread_state.rs | 49 +++++++++++++------------ 2 files changed, 68 insertions(+), 27 deletions(-) diff --git a/codex-rs/app-server/src/current_time.rs b/codex-rs/app-server/src/current_time.rs index 04386cc8ebd3..e1cb424adc32 100644 --- a/codex-rs/app-server/src/current_time.rs +++ b/codex-rs/app-server/src/current_time.rs @@ -16,6 +16,7 @@ use codex_protocol::ThreadId; use tokio::time::Duration; use tokio::time::timeout; +use crate::outgoing_message::ConnectionId; use crate::outgoing_message::OutgoingMessageSender; use crate::thread_state::ThreadStateManager; @@ -54,10 +55,10 @@ async fn request_current_time( thread_state_manager: ThreadStateManager, thread_id: ThreadId, ) -> Result> { - let connection_id = thread_state_manager - .first_current_time_capable_connection_for_thread(thread_id) - .await - .ok_or_else(|| anyhow!("no current-time capable client is subscribed to the thread"))?; + let connection_ids = thread_state_manager + .current_time_capable_connections_for_thread(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( @@ -93,3 +94,40 @@ async fn request_current_time( 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 current-time capable 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 current-time capable 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 current-time capable client subscribed to the thread, found 2" + ); + } +} diff --git a/codex-rs/app-server/src/thread_state.rs b/codex-rs/app-server/src/thread_state.rs index bc2c3d6fbe28..cdaad1fa7e11 100644 --- a/codex-rs/app-server/src/thread_state.rs +++ b/codex-rs/app-server/src/thread_state.rs @@ -313,27 +313,6 @@ impl ThreadStateManager { pub(crate) async fn first_attestation_capable_connection_for_thread( &self, thread_id: ThreadId, - ) -> Option { - self.first_capable_connection_for_thread(thread_id, |capabilities| { - capabilities.request_attestation - }) - .await - } - - pub(crate) async fn first_current_time_capable_connection_for_thread( - &self, - thread_id: ThreadId, - ) -> Option { - self.first_capable_connection_for_thread(thread_id, |capabilities| { - capabilities.request_current_time - }) - .await - } - - async fn first_capable_connection_for_thread( - &self, - thread_id: ThreadId, - is_capable: impl Fn(&ConnectionCapabilities) -> bool, ) -> Option { let state = self.state.lock().await; state @@ -342,12 +321,36 @@ impl ThreadStateManager { .connection_ids .iter() .filter_map(|connection_id| { - let capabilities = state.live_connections.get(connection_id)?; - is_capable(capabilities).then_some(*connection_id) + state + .live_connections + .get(connection_id)? + .request_attestation + .then_some(*connection_id) }) .min_by_key(|connection_id| connection_id.0) } + pub(crate) async fn current_time_capable_connections_for_thread( + &self, + thread_id: ThreadId, + ) -> Vec { + let state = self.state.lock().await; + let Some(thread_entry) = state.threads.get(&thread_id) else { + return Vec::new(); + }; + thread_entry + .connection_ids + .iter() + .filter_map(|connection_id| { + state + .live_connections + .get(connection_id)? + .request_current_time + .then_some(*connection_id) + }) + .collect() + } + pub(crate) async fn subscribed_connection_ids(&self, thread_id: ThreadId) -> Vec { let state = self.state.lock().await; state From abc29ed3a387b96a0ac22ba8abac60a6b96b91ee Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Wed, 17 Jun 2026 20:41:07 -0700 Subject: [PATCH 05/15] Harden current-time client requests --- codex-rs/app-server-protocol/schema/json/ClientRequest.json | 1 - .../schema/json/codex_app_server_protocol.schemas.json | 1 - .../schema/json/codex_app_server_protocol.v2.schemas.json | 1 - .../app-server-protocol/schema/json/v1/InitializeParams.json | 1 - .../schema/typescript/InitializeCapabilities.ts | 2 +- .../schema/typescript/v2/CurrentTimeReadResponse.ts | 2 +- codex-rs/app-server-protocol/src/protocol/common.rs | 1 - codex-rs/app-server-protocol/src/protocol/v1.rs | 2 +- codex-rs/app-server-protocol/src/protocol/v2/current_time.rs | 1 + codex-rs/app-server/src/current_time.rs | 2 +- 10 files changed, 5 insertions(+), 9 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index d045e87154ab..55b833928a8c 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1292,7 +1292,6 @@ "type": "boolean" }, "requestCurrentTime": { - "default": false, "description": "Opt into `currentTime/read` requests for an external clock.", "type": "boolean" } diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index d1536de614ac..fde5106cef45 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -2948,7 +2948,6 @@ "type": "boolean" }, "requestCurrentTime": { - "default": false, "description": "Opt into `currentTime/read` requests for an external clock.", "type": "boolean" } diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index cfc577b2752d..302ee0eaf067 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -7460,7 +7460,6 @@ "type": "boolean" }, "requestCurrentTime": { - "default": false, "description": "Opt into `currentTime/read` requests for an external clock.", "type": "boolean" } diff --git a/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json b/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json index f672bdcae114..df1c54939fc5 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json @@ -50,7 +50,6 @@ "type": "boolean" }, "requestCurrentTime": { - "default": false, "description": "Opt into `currentTime/read` requests for an external clock.", "type": "boolean" } diff --git a/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts b/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts index e9f4073f6957..0b705a346362 100644 --- a/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts +++ b/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts @@ -21,7 +21,7 @@ mcpServerOpenaiFormElicitation?: boolean, /** * Opt into `currentTime/read` requests for an external clock. */ -requestCurrentTime: boolean, +requestCurrentTime?: boolean, /** * Exact notification method names that should be suppressed for this * connection (for example `thread/started`). diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CurrentTimeReadResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CurrentTimeReadResponse.ts index 101400a25c0e..4fcdcafa6d8a 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CurrentTimeReadResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CurrentTimeReadResponse.ts @@ -6,4 +6,4 @@ export type CurrentTimeReadResponse = { /** * Current time as whole Unix seconds. */ -currentTimeAt: bigint, }; +currentTimeAt: number, }; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 90cf8486ce9c..d79aa686366b 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -2192,7 +2192,6 @@ mod tests { "experimentalApi": true, "requestAttestation": true, "mcpServerOpenaiFormElicitation": true, - "requestCurrentTime": false, "optOutNotificationMethods": [ "thread/started", "item/agentMessage/delta" diff --git a/codex-rs/app-server-protocol/src/protocol/v1.rs b/codex-rs/app-server-protocol/src/protocol/v1.rs index 7e74411131eb..b8de816958a2 100644 --- a/codex-rs/app-server-protocol/src/protocol/v1.rs +++ b/codex-rs/app-server-protocol/src/protocol/v1.rs @@ -54,7 +54,7 @@ pub struct InitializeCapabilities { #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub mcp_server_openai_form_elicitation: bool, /// Opt into `currentTime/read` requests for an external clock. - #[serde(default)] + #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub request_current_time: bool, /// Exact notification method names that should be suppressed for this /// connection (for example `thread/started`). 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 index aad197f29cf6..9fd23f798178 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/current_time.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/current_time.rs @@ -15,5 +15,6 @@ pub struct CurrentTimeReadParams { #[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/src/current_time.rs b/codex-rs/app-server/src/current_time.rs index e1cb424adc32..c52e3ca3434c 100644 --- a/codex-rs/app-server/src/current_time.rs +++ b/codex-rs/app-server/src/current_time.rs @@ -66,7 +66,7 @@ async fn request_current_time( ServerRequestPayload::CurrentTimeRead(CurrentTimeReadParams { thread_id: thread_id.to_string(), }), - Some(thread_id), + /*thread_id*/ None, ) .await; From aea99c15a613991d99e8364b1beb8787e2bfac0d Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Wed, 17 Jun 2026 20:50:14 -0700 Subject: [PATCH 06/15] Wait for current-time subscriber attachment --- codex-rs/app-server/src/current_time.rs | 28 +++++++++++++++---- .../thread_processor_tests.rs | 25 +++++++++++++++++ codex-rs/app-server/src/thread_state.rs | 17 +++++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/codex-rs/app-server/src/current_time.rs b/codex-rs/app-server/src/current_time.rs index c52e3ca3434c..9d3a97228eab 100644 --- a/codex-rs/app-server/src/current_time.rs +++ b/codex-rs/app-server/src/current_time.rs @@ -14,13 +14,14 @@ use codex_core::CurrentTimeFuture; use codex_core::CurrentTimeProvider; use codex_protocol::ThreadId; use tokio::time::Duration; -use tokio::time::timeout; +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_READ_TIMEOUT: Duration = Duration::from_secs(5); +const CURRENT_TIME_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); pub(crate) fn app_server_current_time_provider( outgoing: Arc, @@ -55,9 +56,26 @@ async fn request_current_time( thread_state_manager: ThreadStateManager, thread_id: ThreadId, ) -> Result> { - let connection_ids = thread_state_manager + let deadline = Instant::now() + CURRENT_TIME_REQUEST_TIMEOUT; + let mut connection_ids = thread_state_manager .current_time_capable_connections_for_thread(thread_id) .await; + if connection_ids.is_empty() { + 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() + ) + })?; + connection_ids = thread_state_manager + .current_time_capable_connections_for_thread(thread_id) + .await; + } let connection_id = require_single_current_time_connection(&connection_ids)?; let connection_ids = [connection_id]; let (request_id, rx) = outgoing @@ -70,7 +88,7 @@ async fn request_current_time( ) .await; - let result = match timeout(CURRENT_TIME_READ_TIMEOUT, rx).await { + let result = match timeout_at(deadline, rx).await { Ok(Ok(Ok(result))) => result, Ok(Ok(Err(err))) => { bail!( @@ -84,7 +102,7 @@ async fn request_current_time( let _canceled = outgoing.cancel_request(&request_id).await; bail!( "current-time request timed out after {}s", - CURRENT_TIME_READ_TIMEOUT.as_secs() + CURRENT_TIME_REQUEST_TIMEOUT.as_secs() ); } }; diff --git a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs index 65b409713754..84752825aa60 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs @@ -1381,6 +1381,31 @@ mod thread_processor_behavior_tests { Ok(()) } + #[tokio::test] + async fn wait_for_thread_subscriber_unblocks_after_connection_attaches() -> Result<()> { + let manager = ThreadStateManager::new(); + let thread_id = ThreadId::from_string("ba62fd70-2ec2-4b1b-9d94-355694332dd2")?; + let connection = ConnectionId(1); + manager + .connection_initialized(connection, ConnectionCapabilities::default()) + .await; + + let wait_for_subscriber = manager.wait_for_thread_subscriber(thread_id); + let attach_connection = async { + tokio::task::yield_now().await; + manager + .try_add_connection_to_thread(thread_id, connection) + .await + }; + let ((), attached) = tokio::time::timeout(Duration::from_secs(1), async { + tokio::join!(wait_for_subscriber, attach_connection) + }) + .await?; + + assert!(attached); + Ok(()) + } + #[tokio::test] async fn closed_connection_cannot_be_reintroduced_by_auto_subscribe() -> Result<()> { let manager = ThreadStateManager::new(); diff --git a/codex-rs/app-server/src/thread_state.rs b/codex-rs/app-server/src/thread_state.rs index cdaad1fa7e11..daead9ccc614 100644 --- a/codex-rs/app-server/src/thread_state.rs +++ b/codex-rs/app-server/src/thread_state.rs @@ -351,6 +351,23 @@ impl ThreadStateManager { .collect() } + pub(crate) async fn wait_for_thread_subscriber(&self, thread_id: ThreadId) { + let mut has_connections = { + let mut state = self.state.lock().await; + state + .threads + .entry(thread_id) + .or_default() + .has_connections_watcher + .subscribe() + }; + while !*has_connections.borrow_and_update() { + if has_connections.changed().await.is_err() { + break; + } + } + } + pub(crate) async fn subscribed_connection_ids(&self, thread_id: ThreadId) -> Vec { let state = self.state.lock().await; state From c2b47be7495c2efb6160408891076e8d823c4788 Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Wed, 17 Jun 2026 21:02:06 -0700 Subject: [PATCH 07/15] Document current-time client scope --- codex-rs/app-server/src/current_time.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/codex-rs/app-server/src/current_time.rs b/codex-rs/app-server/src/current_time.rs index 9d3a97228eab..52d6172686ee 100644 --- a/codex-rs/app-server/src/current_time.rs +++ b/codex-rs/app-server/src/current_time.rs @@ -61,6 +61,10 @@ async fn request_current_time( .current_time_capable_connections_for_thread(thread_id) .await; if connection_ids.is_empty() { + // External current time only needs to support a single app-server client for now. Wait for + // that client to subscribe, then verify below that it advertised `requestCurrentTime`. + // Supporting multiple clients would require a capability-aware wait, which adds complexity + // we do not currently need. timeout_at( deadline, thread_state_manager.wait_for_thread_subscriber(thread_id), From 716adcb08d468997b7f3914dd9bbd7a8fdcf436f Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Wed, 17 Jun 2026 21:02:52 -0700 Subject: [PATCH 08/15] Tighten current-time scope comment --- codex-rs/app-server/src/current_time.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/codex-rs/app-server/src/current_time.rs b/codex-rs/app-server/src/current_time.rs index 52d6172686ee..33b6b8c2803a 100644 --- a/codex-rs/app-server/src/current_time.rs +++ b/codex-rs/app-server/src/current_time.rs @@ -61,8 +61,7 @@ async fn request_current_time( .current_time_capable_connections_for_thread(thread_id) .await; if connection_ids.is_empty() { - // External current time only needs to support a single app-server client for now. Wait for - // that client to subscribe, then verify below that it advertised `requestCurrentTime`. + // External current time only needs to support a single app-server client for now. // Supporting multiple clients would require a capability-aware wait, which adds complexity // we do not currently need. timeout_at( From a30e1bc4ee8708251e3b0cb9f79052e915e04909 Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Wed, 17 Jun 2026 21:03:23 -0700 Subject: [PATCH 09/15] Clarify multi-client clock tradeoff --- codex-rs/app-server/src/current_time.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/app-server/src/current_time.rs b/codex-rs/app-server/src/current_time.rs index 33b6b8c2803a..0e663b359b14 100644 --- a/codex-rs/app-server/src/current_time.rs +++ b/codex-rs/app-server/src/current_time.rs @@ -62,8 +62,8 @@ async fn request_current_time( .await; if connection_ids.is_empty() { // External current time only needs to support a single app-server client for now. - // Supporting multiple clients would require a capability-aware wait, which adds complexity - // we do not currently need. + // Supporting multiple clients without attachment races would require a capability-aware + // wait, adding complexity that is not currently necessary. timeout_at( deadline, thread_state_manager.wait_for_thread_subscriber(thread_id), From b1904b8b96f74975b910449f8049d02ea70317b8 Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Wed, 17 Jun 2026 21:23:16 -0700 Subject: [PATCH 10/15] Handle current-time requests in CLI clients --- codex-rs/exec/src/lib.rs | 9 +++++++++ codex-rs/tui/src/app/app_server_event_targets.rs | 3 +++ codex-rs/tui/src/app/app_server_requests.rs | 7 +++++++ codex-rs/tui/src/app/side.rs | 1 + codex-rs/tui/src/chatwidget/protocol_requests.rs | 1 + 5 files changed, 21 insertions(+) diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index a1f4b95b5006..9745fa878ed7 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -1719,6 +1719,15 @@ 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/app_server_event_targets.rs b/codex-rs/tui/src/app/app_server_event_targets.rs index 90f688f7ed1b..e29d0e9d5d3c 100644 --- a/codex-rs/tui/src/app/app_server_event_targets.rs +++ b/codex-rs/tui/src/app/app_server_event_targets.rs @@ -24,6 +24,9 @@ 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_requests.rs b/codex-rs/tui/src/app/app_server_requests.rs index d61e84dcec54..0b728976853b 100644 --- a/codex-rs/tui/src/app/app_server_requests.rs +++ b/codex-rs/tui/src/app/app_server_requests.rs @@ -153,6 +153,12 @@ 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(), @@ -352,6 +358,7 @@ impl PendingAppServerRequests { ServerRequest::DynamicToolCall { .. } | ServerRequest::ChatgptAuthTokensRefresh { .. } | ServerRequest::AttestationGenerate { .. } + | ServerRequest::CurrentTimeRead { .. } | ServerRequest::ApplyPatchApproval { .. } | ServerRequest::ExecCommandApproval { .. } => true, } diff --git a/codex-rs/tui/src/app/side.rs b/codex-rs/tui/src/app/side.rs index 167fc5c7cb34..0b69153fa5c6 100644 --- a/codex-rs/tui/src/app/side.rs +++ b/codex-rs/tui/src/app/side.rs @@ -97,6 +97,7 @@ 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 2f33c9809a84..5729c073fd65 100644 --- a/codex-rs/tui/src/chatwidget/protocol_requests.rs +++ b/codex-rs/tui/src/chatwidget/protocol_requests.rs @@ -46,6 +46,7 @@ impl ChatWidget { } ServerRequest::DynamicToolCall { .. } | ServerRequest::AttestationGenerate { .. } + | ServerRequest::CurrentTimeRead { .. } | ServerRequest::ChatgptAuthTokensRefresh { .. } | ServerRequest::ApplyPatchApproval { .. } | ServerRequest::ExecCommandApproval { .. } => { From b4a3d363ba09228c26c287392d0d7f45b6f53e8d Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Thu, 18 Jun 2026 05:48:40 -0700 Subject: [PATCH 11/15] Document external current time source --- codex-rs/app-server/README.md | 2 +- codex-rs/app-server/tests/suite/v2/current_time.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 5672245d60a7..dd8afa44b684 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -1471,7 +1471,7 @@ Desktop hosts that provide upstream attestation should set `capabilities.request ### Current time -Hosts that provide an external clock should set `capabilities.requestCurrentTime` during `initialize` and handle the server-initiated `currentTime/read` request. When `[features.current_time_reminder]` is enabled with `clock_source = "app_server_client"`, app-server sends `{ "threadId": "thr_123" }` when a time reminder is due. The client responds with `{ "currentTimeAt": 1781717655 }`, where `currentTimeAt` is an integer Unix timestamp in seconds. A failed, canceled, timed-out, or malformed response stops the turn before the model request is sent. +Hosts that provide an external clock should set `capabilities.requestCurrentTime` during `initialize` and handle the server-initiated `currentTime/read` request. When `[features.current_time_reminder]` is enabled with `clock_source = "external"`, app-server sends `{ "threadId": "thr_123" }` when a time reminder is due. The client responds with `{ "currentTimeAt": 1781717655 }`, where `currentTimeAt` is an integer Unix timestamp in seconds. A failed, canceled, timed-out, or malformed response stops the turn before the model request is sent. ### MCP server elicitations 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 4bddd9597d93..446fe51eaa0c 100644 --- a/codex-rs/app-server/tests/suite/v2/current_time.rs +++ b/codex-rs/app-server/tests/suite/v2/current_time.rs @@ -138,7 +138,7 @@ model_provider = "mock_provider" [features.current_time_reminder] enabled = true reminder_interval_model_requests = 1 -clock_source = "app_server_client" +clock_source = "external" [model_providers.mock_provider] name = "Mock provider for test" From 2f843ec2f74809a74df0e9857e4e124b6c04bc92 Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Thu, 18 Jun 2026 06:08:28 -0700 Subject: [PATCH 12/15] Rename app server time provider --- codex-rs/app-server/src/current_time.rs | 16 ++++++++-------- codex-rs/app-server/src/message_processor.rs | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/codex-rs/app-server/src/current_time.rs b/codex-rs/app-server/src/current_time.rs index 0e663b359b14..a4cdd2fec5b3 100644 --- a/codex-rs/app-server/src/current_time.rs +++ b/codex-rs/app-server/src/current_time.rs @@ -10,8 +10,8 @@ use chrono::Utc; use codex_app_server_protocol::CurrentTimeReadParams; use codex_app_server_protocol::CurrentTimeReadResponse; use codex_app_server_protocol::ServerRequestPayload; -use codex_core::CurrentTimeFuture; -use codex_core::CurrentTimeProvider; +use codex_core::TimeFuture; +use codex_core::TimeProvider; use codex_protocol::ThreadId; use tokio::time::Duration; use tokio::time::Instant; @@ -23,23 +23,23 @@ use crate::thread_state::ThreadStateManager; const CURRENT_TIME_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); -pub(crate) fn app_server_current_time_provider( +pub(crate) fn app_server_time_provider( outgoing: Arc, thread_state_manager: ThreadStateManager, -) -> Arc { - Arc::new(AppServerCurrentTimeProvider { +) -> Arc { + Arc::new(AppServerTimeProvider { outgoing: Arc::downgrade(&outgoing), thread_state_manager, }) } -struct AppServerCurrentTimeProvider { +struct AppServerTimeProvider { outgoing: Weak, thread_state_manager: ThreadStateManager, } -impl CurrentTimeProvider for AppServerCurrentTimeProvider { - fn current_time(&self, thread_id: ThreadId) -> CurrentTimeFuture<'_> { +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 { diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index d51fefa8f6a9..b5896cda98ef 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -7,7 +7,7 @@ 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_current_time_provider; +use crate::current_time::app_server_time_provider; use crate::error_code::invalid_request; use crate::extensions::ThreadExtensionDependencies; use crate::extensions::app_server_extension_event_sink; @@ -387,7 +387,7 @@ impl MessageProcessor { outgoing.clone(), thread_state_manager.clone(), )), - Some(app_server_current_time_provider( + Some(app_server_time_provider( outgoing.clone(), thread_state_manager.clone(), )), From 8299b26bab73a500285d4a777383a81703df006f Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Thu, 18 Jun 2026 09:51:55 -0700 Subject: [PATCH 13/15] Drop current time capability negotiation --- .../analytics/src/analytics_client_tests.rs | 4 -- codex-rs/app-server-client/src/lib.rs | 1 - codex-rs/app-server-client/src/remote.rs | 1 - .../schema/json/ClientRequest.json | 4 -- .../codex_app_server_protocol.schemas.json | 4 -- .../codex_app_server_protocol.v2.schemas.json | 4 -- .../schema/json/v1/InitializeParams.json | 4 -- .../typescript/InitializeCapabilities.ts | 4 -- .../src/protocol/common.rs | 2 - .../app-server-protocol/src/protocol/v1.rs | 3 -- codex-rs/app-server-test-client/src/lib.rs | 1 - codex-rs/app-server/README.md | 2 +- codex-rs/app-server/src/current_time.rs | 40 ++++++++----------- codex-rs/app-server/src/lib.rs | 3 -- codex-rs/app-server/src/message_processor.rs | 11 ----- .../initialize_processor.rs | 2 - codex-rs/app-server/src/thread_state.rs | 22 ---------- .../app-server/tests/suite/v2/attestation.rs | 1 - .../app-server/tests/suite/v2/current_time.rs | 24 +---------- .../tests/suite/v2/experimental_api.rs | 8 ---- .../app-server/tests/suite/v2/initialize.rs | 1 - .../tests/suite/v2/thread_status.rs | 1 - 22 files changed, 18 insertions(+), 129 deletions(-) diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 2e687b85059b..fcdd97af8ec1 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -773,7 +773,6 @@ fn sample_initialize_fact(connection_id: u64) -> AnalyticsFact { capabilities: Some(InitializeCapabilities { experimental_api: false, request_attestation: false, - request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), @@ -1670,7 +1669,6 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize capabilities: Some(InitializeCapabilities { experimental_api: false, request_attestation: false, - request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), @@ -1821,7 +1819,6 @@ async fn compaction_event_ingests_custom_fact() { capabilities: Some(InitializeCapabilities { experimental_api: false, request_attestation: false, - request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), @@ -1951,7 +1948,6 @@ async fn guardian_review_event_ingests_custom_fact_with_optional_target_item() { capabilities: Some(InitializeCapabilities { experimental_api: false, request_attestation: false, - request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index faecbc9bf2a4..b7a958eff1f5 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -371,7 +371,6 @@ impl InProcessClientStartArgs { let capabilities = InitializeCapabilities { experimental_api: self.experimental_api, request_attestation: false, - request_current_time: false, opt_out_notification_methods: if self.opt_out_notification_methods.is_empty() { None } else { diff --git a/codex-rs/app-server-client/src/remote.rs b/codex-rs/app-server-client/src/remote.rs index cda250feb291..5575fe4ad46a 100644 --- a/codex-rs/app-server-client/src/remote.rs +++ b/codex-rs/app-server-client/src/remote.rs @@ -95,7 +95,6 @@ impl RemoteAppServerConnectArgs { let capabilities = InitializeCapabilities { experimental_api: self.experimental_api, request_attestation: false, - request_current_time: false, opt_out_notification_methods: if self.opt_out_notification_methods.is_empty() { None } else { diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 55b833928a8c..e183c88b08af 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1290,10 +1290,6 @@ "default": false, "description": "Opt into `attestation/generate` requests for upstream `x-oai-attestation`.", "type": "boolean" - }, - "requestCurrentTime": { - "description": "Opt into `currentTime/read` requests for an external clock.", - "type": "boolean" } }, "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index fde5106cef45..a3aa9e9dea7f 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -2946,10 +2946,6 @@ "default": false, "description": "Opt into `attestation/generate` requests for upstream `x-oai-attestation`.", "type": "boolean" - }, - "requestCurrentTime": { - "description": "Opt into `currentTime/read` requests for an external clock.", - "type": "boolean" } }, "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 302ee0eaf067..00d7a4bbdfac 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -7458,10 +7458,6 @@ "default": false, "description": "Opt into `attestation/generate` requests for upstream `x-oai-attestation`.", "type": "boolean" - }, - "requestCurrentTime": { - "description": "Opt into `currentTime/read` requests for an external clock.", - "type": "boolean" } }, "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json b/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json index df1c54939fc5..75f0860ddda5 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json @@ -48,10 +48,6 @@ "default": false, "description": "Opt into `attestation/generate` requests for upstream `x-oai-attestation`.", "type": "boolean" - }, - "requestCurrentTime": { - "description": "Opt into `currentTime/read` requests for an external clock.", - "type": "boolean" } }, "type": "object" diff --git a/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts b/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts index 0b705a346362..dcc4dffb0b51 100644 --- a/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts +++ b/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts @@ -18,10 +18,6 @@ requestAttestation: boolean, * Allow downstream MCP servers to request OpenAI extended form elicitations. */ mcpServerOpenaiFormElicitation?: boolean, -/** - * Opt into `currentTime/read` requests for an external clock. - */ -requestCurrentTime?: boolean, /** * Exact notification method names that should be suppressed for this * connection (for example `thread/started`). diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index d79aa686366b..c49ff8b0f743 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -2169,7 +2169,6 @@ mod tests { experimental_api: true, request_attestation: true, mcp_server_openai_form_elicitation: true, - request_current_time: false, opt_out_notification_methods: Some(vec![ "thread/started".to_string(), "item/agentMessage/delta".to_string(), @@ -2241,7 +2240,6 @@ mod tests { experimental_api: true, request_attestation: true, mcp_server_openai_form_elicitation: true, - request_current_time: false, opt_out_notification_methods: Some(vec![ "thread/started".to_string(), "item/agentMessage/delta".to_string(), diff --git a/codex-rs/app-server-protocol/src/protocol/v1.rs b/codex-rs/app-server-protocol/src/protocol/v1.rs index b8de816958a2..dccea51a3601 100644 --- a/codex-rs/app-server-protocol/src/protocol/v1.rs +++ b/codex-rs/app-server-protocol/src/protocol/v1.rs @@ -53,9 +53,6 @@ pub struct InitializeCapabilities { /// Allow downstream MCP servers to request OpenAI extended form elicitations. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub mcp_server_openai_form_elicitation: bool, - /// Opt into `currentTime/read` requests for an external clock. - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub request_current_time: bool, /// Exact notification method names that should be suppressed for this /// connection (for example `thread/started`). #[ts(optional = nullable)] diff --git a/codex-rs/app-server-test-client/src/lib.rs b/codex-rs/app-server-test-client/src/lib.rs index d17397e3857c..856d75585ec3 100644 --- a/codex-rs/app-server-test-client/src/lib.rs +++ b/codex-rs/app-server-test-client/src/lib.rs @@ -1662,7 +1662,6 @@ impl CodexClient { capabilities: Some(InitializeCapabilities { experimental_api, request_attestation: false, - request_current_time: false, opt_out_notification_methods: Some( NOTIFICATIONS_TO_OPT_OUT .iter() diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index dd8afa44b684..f45e93018090 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -1471,7 +1471,7 @@ Desktop hosts that provide upstream attestation should set `capabilities.request ### Current time -Hosts that provide an external clock should set `capabilities.requestCurrentTime` during `initialize` and handle the server-initiated `currentTime/read` request. When `[features.current_time_reminder]` is enabled with `clock_source = "external"`, app-server sends `{ "threadId": "thr_123" }` when a time reminder is due. The client responds with `{ "currentTimeAt": 1781717655 }`, where `currentTimeAt` is an integer Unix timestamp in seconds. A failed, canceled, timed-out, or malformed response stops the turn before the model request is sent. +When `[features.current_time_reminder]` is enabled with `clock_source = "external"`, app-server sends the client subscribed to the thread a `currentTime/read` request with `{ "threadId": "thr_123" }` when a time reminder is due. The client responds with `{ "currentTimeAt": 1781717655 }`, where `currentTimeAt` is an integer Unix timestamp in seconds. A failed, canceled, timed-out, or malformed response stops the turn before the model request is sent. ### MCP server elicitations diff --git a/codex-rs/app-server/src/current_time.rs b/codex-rs/app-server/src/current_time.rs index a4cdd2fec5b3..9b76d337a2c7 100644 --- a/codex-rs/app-server/src/current_time.rs +++ b/codex-rs/app-server/src/current_time.rs @@ -57,28 +57,20 @@ async fn request_current_time( thread_id: ThreadId, ) -> Result> { let deadline = Instant::now() + CURRENT_TIME_REQUEST_TIMEOUT; - let mut connection_ids = thread_state_manager - .current_time_capable_connections_for_thread(thread_id) - .await; - if connection_ids.is_empty() { - // External current time only needs to support a single app-server client for now. - // Supporting multiple clients without attachment races would require a capability-aware - // wait, adding complexity that is not currently necessary. - timeout_at( - deadline, - thread_state_manager.wait_for_thread_subscriber(thread_id), + 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() ) - .await - .map_err(|_| { - anyhow!( - "timed out waiting for a client to subscribe to the thread after {}s", - CURRENT_TIME_REQUEST_TIMEOUT.as_secs() - ) - })?; - connection_ids = thread_state_manager - .current_time_capable_connections_for_thread(thread_id) - .await; - } + })?; + 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 @@ -121,7 +113,7 @@ fn require_single_current_time_connection(connection_ids: &[ConnectionId]) -> Re match connection_ids { [connection_id] => Ok(*connection_id), _ => bail!( - "expected exactly one current-time capable client subscribed to the thread, found {}", + "expected exactly one client subscribed to the thread, found {}", connection_ids.len() ), } @@ -142,13 +134,13 @@ mod tests { require_single_current_time_connection(&[]) .unwrap_err() .to_string(), - "expected exactly one current-time capable client subscribed to the thread, found 0" + "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 current-time capable client subscribed to the thread, found 2" + "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 f66954950777..f4cd83d732de 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -1069,9 +1069,6 @@ pub async fn run_main_with_transport_options( connection_state .session .request_attestation(), - connection_state - .session - .request_current_time(), ) .await; connection_state diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index b5896cda98ef..ad29e05010b8 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -225,7 +225,6 @@ pub(crate) struct InitializedConnectionSessionState { pub(crate) client_version: String, pub(crate) request_attestation: bool, pub(crate) supports_openai_form_elicitation: bool, - pub(crate) request_current_time: bool, } impl Default for ConnectionSessionState { @@ -282,13 +281,6 @@ impl ConnectionSessionState { .get() .is_some_and(|session| session.supports_openai_form_elicitation) } - - pub(crate) fn request_current_time(&self) -> bool { - self.initialized - .get() - .is_some_and(|session| session.request_current_time) - } - pub(crate) fn initialize(&self, session: InitializedConnectionSessionState) -> Result<(), ()> { self.initialized.set(session).map_err(|_| ()) } @@ -741,14 +733,12 @@ impl MessageProcessor { &self, connection_id: ConnectionId, request_attestation: bool, - request_current_time: bool, ) { self.thread_processor .connection_initialized( connection_id, ConnectionCapabilities { request_attestation, - request_current_time, }, ) .await; @@ -863,7 +853,6 @@ impl MessageProcessor { connection_id, ConnectionCapabilities { request_attestation: session.request_attestation(), - request_current_time: session.request_current_time(), }, ) .await; diff --git a/codex-rs/app-server/src/request_processors/initialize_processor.rs b/codex-rs/app-server/src/request_processors/initialize_processor.rs index bc0497f65df3..cfdad27f50ff 100644 --- a/codex-rs/app-server/src/request_processors/initialize_processor.rs +++ b/codex-rs/app-server/src/request_processors/initialize_processor.rs @@ -71,7 +71,6 @@ impl InitializeRequestProcessor { let experimental_api_enabled = capabilities.experimental_api; let request_attestation = capabilities.request_attestation; let supports_openai_form_elicitation = capabilities.mcp_server_openai_form_elicitation; - let request_current_time = capabilities.request_current_time; let opt_out_notification_methods = capabilities .opt_out_notification_methods .unwrap_or_default(); @@ -99,7 +98,6 @@ impl InitializeRequestProcessor { client_version: version, request_attestation, supports_openai_form_elicitation, - request_current_time, }) .is_err() { diff --git a/codex-rs/app-server/src/thread_state.rs b/codex-rs/app-server/src/thread_state.rs index daead9ccc614..890d4da5ebeb 100644 --- a/codex-rs/app-server/src/thread_state.rs +++ b/codex-rs/app-server/src/thread_state.rs @@ -281,7 +281,6 @@ struct ThreadStateManagerInner { #[derive(Clone, Copy, Default)] pub(crate) struct ConnectionCapabilities { pub(crate) request_attestation: bool, - pub(crate) request_current_time: bool, } #[derive(Clone, Default)] @@ -330,27 +329,6 @@ impl ThreadStateManager { .min_by_key(|connection_id| connection_id.0) } - pub(crate) async fn current_time_capable_connections_for_thread( - &self, - thread_id: ThreadId, - ) -> Vec { - let state = self.state.lock().await; - let Some(thread_entry) = state.threads.get(&thread_id) else { - return Vec::new(); - }; - thread_entry - .connection_ids - .iter() - .filter_map(|connection_id| { - state - .live_connections - .get(connection_id)? - .request_current_time - .then_some(*connection_id) - }) - .collect() - } - pub(crate) async fn wait_for_thread_subscriber(&self, thread_id: ThreadId) { let mut has_connections = { let mut state = self.state.lock().await; diff --git a/codex-rs/app-server/tests/suite/v2/attestation.rs b/codex-rs/app-server/tests/suite/v2/attestation.rs index 6c4458f4a1fc..ea567a2d43cb 100644 --- a/codex-rs/app-server/tests/suite/v2/attestation.rs +++ b/codex-rs/app-server/tests/suite/v2/attestation.rs @@ -80,7 +80,6 @@ async fn attestation_generate_round_trip_adds_header_to_responses_websocket_hand Some(InitializeCapabilities { experimental_api: true, request_attestation: true, - request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), 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 446fe51eaa0c..74e734ae3996 100644 --- a/codex-rs/app-server/tests/suite/v2/current_time.rs +++ b/codex-rs/app-server/tests/suite/v2/current_time.rs @@ -1,14 +1,10 @@ use std::path::Path; use anyhow::Result; -use app_test_support::DEFAULT_CLIENT_NAME; use app_test_support::TestAppServer; use app_test_support::create_final_assistant_message_sse_response; use app_test_support::to_response; -use codex_app_server_protocol::ClientInfo; use codex_app_server_protocol::CurrentTimeReadResponse; -use codex_app_server_protocol::InitializeCapabilities; -use codex_app_server_protocol::JSONRPCMessage; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerRequest; @@ -45,25 +41,7 @@ async fn current_time_read_round_trip_adds_reminder_to_model_input() -> Result<( create_config_toml(codex_home.path(), &server.uri())?; let mut app_server = TestAppServer::new(codex_home.path()).await?; - let initialized = timeout( - DEFAULT_READ_TIMEOUT, - app_server.initialize_with_capabilities( - ClientInfo { - name: DEFAULT_CLIENT_NAME.to_string(), - title: None, - version: "0.1.0".to_string(), - }, - Some(InitializeCapabilities { - experimental_api: true, - request_current_time: true, - ..Default::default() - }), - ), - ) - .await??; - let JSONRPCMessage::Response(_) = initialized else { - panic!("expected initialize response, got: {initialized:?}"); - }; + timeout(DEFAULT_READ_TIMEOUT, app_server.initialize()).await??; let thread_request_id = app_server .send_thread_start_request(ThreadStartParams::default()) diff --git a/codex-rs/app-server/tests/suite/v2/experimental_api.rs b/codex-rs/app-server/tests/suite/v2/experimental_api.rs index 83b27fb969b9..62ad4c067784 100644 --- a/codex-rs/app-server/tests/suite/v2/experimental_api.rs +++ b/codex-rs/app-server/tests/suite/v2/experimental_api.rs @@ -38,7 +38,6 @@ async fn mock_experimental_method_requires_experimental_api_capability() -> Resu Some(InitializeCapabilities { experimental_api: false, request_attestation: false, - request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), @@ -71,7 +70,6 @@ async fn realtime_conversation_start_requires_experimental_api_capability() -> R Some(InitializeCapabilities { experimental_api: false, request_attestation: false, - request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), @@ -119,7 +117,6 @@ async fn thread_memory_mode_set_requires_experimental_api_capability() -> Result Some(InitializeCapabilities { experimental_api: false, request_attestation: false, - request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), @@ -155,7 +152,6 @@ async fn thread_settings_update_requires_experimental_api_capability() -> Result Some(InitializeCapabilities { experimental_api: false, request_attestation: false, - request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), @@ -191,7 +187,6 @@ async fn realtime_webrtc_start_requires_experimental_api_capability() -> Result< Some(InitializeCapabilities { experimental_api: false, request_attestation: false, - request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), @@ -243,7 +238,6 @@ async fn thread_start_mock_field_requires_experimental_api_capability() -> Resul Some(InitializeCapabilities { experimental_api: false, request_attestation: false, - request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), @@ -283,7 +277,6 @@ async fn thread_start_without_dynamic_tools_allows_without_experimental_api_capa Some(InitializeCapabilities { experimental_api: false, request_attestation: false, - request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), @@ -322,7 +315,6 @@ async fn thread_start_granular_approval_policy_requires_experimental_api_capabil Some(InitializeCapabilities { experimental_api: false, request_attestation: false, - request_current_time: false, opt_out_notification_methods: None, mcp_server_openai_form_elicitation: false, }), diff --git a/codex-rs/app-server/tests/suite/v2/initialize.rs b/codex-rs/app-server/tests/suite/v2/initialize.rs index 8c76c4dbbce4..6fb7aa3bc314 100644 --- a/codex-rs/app-server/tests/suite/v2/initialize.rs +++ b/codex-rs/app-server/tests/suite/v2/initialize.rs @@ -213,7 +213,6 @@ async fn initialize_opt_out_notification_methods_filters_notifications() -> Resu Some(InitializeCapabilities { experimental_api: true, request_attestation: false, - request_current_time: false, opt_out_notification_methods: Some(vec!["thread/started".to_string()]), mcp_server_openai_form_elicitation: false, }), diff --git a/codex-rs/app-server/tests/suite/v2/thread_status.rs b/codex-rs/app-server/tests/suite/v2/thread_status.rs index f646d7bced2e..e922bd13c14e 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_status.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_status.rs @@ -147,7 +147,6 @@ async fn thread_status_changed_can_be_opted_out() -> Result<()> { Some(InitializeCapabilities { experimental_api: true, request_attestation: false, - request_current_time: false, opt_out_notification_methods: Some(vec!["thread/status/changed".to_string()]), mcp_server_openai_form_elicitation: false, }), From 8a2a1df9a7eefd75683d2ca92e4f780722638815 Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Thu, 18 Jun 2026 10:41:44 -0700 Subject: [PATCH 14/15] Remove stale connection capability defaults --- .../src/request_processors/thread_processor_tests.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs index 84752825aa60..3e336cc2a5be 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs @@ -1446,7 +1446,6 @@ mod thread_processor_behavior_tests { unrelated_supported_connection, ConnectionCapabilities { request_attestation: true, - ..Default::default() }, ) .await; @@ -1455,7 +1454,6 @@ mod thread_processor_behavior_tests { earlier_supported_connection, ConnectionCapabilities { request_attestation: true, - ..Default::default() }, ) .await; @@ -1464,7 +1462,6 @@ mod thread_processor_behavior_tests { later_supported_connection, ConnectionCapabilities { request_attestation: true, - ..Default::default() }, ) .await; From 928092097eb5a74fab404450c9272eca4bd6c7ab Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Thu, 18 Jun 2026 11:13:04 -0700 Subject: [PATCH 15/15] Mark current time request experimental --- .../schema/json/CurrentTimeReadParams.json | 13 ----- .../schema/json/CurrentTimeReadResponse.json | 15 ------ .../schema/json/ServerRequest.json | 36 ------------- .../codex_app_server_protocol.schemas.json | 53 ------------------- .../schema/typescript/ServerRequest.ts | 3 +- .../typescript/v2/CurrentTimeReadParams.ts | 5 -- .../typescript/v2/CurrentTimeReadResponse.ts | 9 ---- .../schema/typescript/v2/index.ts | 2 - codex-rs/app-server-protocol/src/export.rs | 50 ++++++++++++----- .../src/protocol/common.rs | 24 +++++++-- codex-rs/app-server/README.md | 2 +- 11 files changed, 60 insertions(+), 152 deletions(-) delete mode 100644 codex-rs/app-server-protocol/schema/json/CurrentTimeReadParams.json delete mode 100644 codex-rs/app-server-protocol/schema/json/CurrentTimeReadResponse.json delete mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/CurrentTimeReadParams.ts delete mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/CurrentTimeReadResponse.ts diff --git a/codex-rs/app-server-protocol/schema/json/CurrentTimeReadParams.json b/codex-rs/app-server-protocol/schema/json/CurrentTimeReadParams.json deleted file mode 100644 index f86754493cb5..000000000000 --- a/codex-rs/app-server-protocol/schema/json/CurrentTimeReadParams.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "threadId": { - "type": "string" - } - }, - "required": [ - "threadId" - ], - "title": "CurrentTimeReadParams", - "type": "object" -} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/CurrentTimeReadResponse.json b/codex-rs/app-server-protocol/schema/json/CurrentTimeReadResponse.json deleted file mode 100644 index 592cd6188b7b..000000000000 --- a/codex-rs/app-server-protocol/schema/json/CurrentTimeReadResponse.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "currentTimeAt": { - "description": "Current time as whole Unix seconds.", - "format": "int64", - "type": "integer" - } - }, - "required": [ - "currentTimeAt" - ], - "title": "CurrentTimeReadResponse", - "type": "object" -} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index e5e4a0724c95..c6ac80dee514 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -448,17 +448,6 @@ ], "type": "object" }, - "CurrentTimeReadParams": { - "properties": { - "threadId": { - "type": "string" - } - }, - "required": [ - "threadId" - ], - "type": "object" - }, "DynamicToolCallParams": { "properties": { "arguments": true, @@ -2005,31 +1994,6 @@ "title": "Attestation/generateRequest", "type": "object" }, - { - "description": "Read the current time from an external clock owned by the client.", - "properties": { - "id": { - "$ref": "#/definitions/RequestId" - }, - "method": { - "enum": [ - "currentTime/read" - ], - "title": "CurrentTime/readRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/CurrentTimeReadParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "CurrentTime/readRequest", - "type": "object" - }, { "description": "DEPRECATED APIs below Request to approve a patch. This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index a3aa9e9dea7f..8ef80b7b542a 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -2469,34 +2469,6 @@ "title": "CommandExecutionRequestApprovalResponse", "type": "object" }, - "CurrentTimeReadParams": { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "threadId": { - "type": "string" - } - }, - "required": [ - "threadId" - ], - "title": "CurrentTimeReadParams", - "type": "object" - }, - "CurrentTimeReadResponse": { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "currentTimeAt": { - "description": "Current time as whole Unix seconds.", - "format": "int64", - "type": "integer" - } - }, - "required": [ - "currentTimeAt" - ], - "title": "CurrentTimeReadResponse", - "type": "object" - }, "DynamicToolCallParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -5666,31 +5638,6 @@ "title": "Attestation/generateRequest", "type": "object" }, - { - "description": "Read the current time from an external clock owned by the client.", - "properties": { - "id": { - "$ref": "#/definitions/v2/RequestId" - }, - "method": { - "enum": [ - "currentTime/read" - ], - "title": "CurrentTime/readRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/CurrentTimeReadParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "CurrentTime/readRequest", - "type": "object" - }, { "description": "DEPRECATED APIs below Request to approve a patch. This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).", "properties": { diff --git a/codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts index a6eaccb77a1d..89a544005648 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts @@ -7,7 +7,6 @@ import type { RequestId } from "./RequestId"; import type { AttestationGenerateParams } from "./v2/AttestationGenerateParams"; import type { ChatgptAuthTokensRefreshParams } from "./v2/ChatgptAuthTokensRefreshParams"; import type { CommandExecutionRequestApprovalParams } from "./v2/CommandExecutionRequestApprovalParams"; -import type { CurrentTimeReadParams } from "./v2/CurrentTimeReadParams"; import type { DynamicToolCallParams } from "./v2/DynamicToolCallParams"; import type { FileChangeRequestApprovalParams } from "./v2/FileChangeRequestApprovalParams"; import type { McpServerElicitationRequestParams } from "./v2/McpServerElicitationRequestParams"; @@ -17,4 +16,4 @@ import type { ToolRequestUserInputParams } from "./v2/ToolRequestUserInputParams /** * Request initiated from the server and sent to the client. */ -export type ServerRequest = { "method": "item/commandExecution/requestApproval", id: RequestId, params: CommandExecutionRequestApprovalParams, } | { "method": "item/fileChange/requestApproval", id: RequestId, params: FileChangeRequestApprovalParams, } | { "method": "item/tool/requestUserInput", id: RequestId, params: ToolRequestUserInputParams, } | { "method": "mcpServer/elicitation/request", id: RequestId, params: McpServerElicitationRequestParams, } | { "method": "item/permissions/requestApproval", id: RequestId, params: PermissionsRequestApprovalParams, } | { "method": "item/tool/call", id: RequestId, params: DynamicToolCallParams, } | { "method": "account/chatgptAuthTokens/refresh", id: RequestId, params: ChatgptAuthTokensRefreshParams, } | { "method": "attestation/generate", id: RequestId, params: AttestationGenerateParams, } | { "method": "currentTime/read", id: RequestId, params: CurrentTimeReadParams, } | { "method": "applyPatchApproval", id: RequestId, params: ApplyPatchApprovalParams, } | { "method": "execCommandApproval", id: RequestId, params: ExecCommandApprovalParams, }; +export type ServerRequest ={ "method": "item/commandExecution/requestApproval", id: RequestId, params: CommandExecutionRequestApprovalParams, } | { "method": "item/fileChange/requestApproval", id: RequestId, params: FileChangeRequestApprovalParams, } | { "method": "item/tool/requestUserInput", id: RequestId, params: ToolRequestUserInputParams, } | { "method": "mcpServer/elicitation/request", id: RequestId, params: McpServerElicitationRequestParams, } | { "method": "item/permissions/requestApproval", id: RequestId, params: PermissionsRequestApprovalParams, } | { "method": "item/tool/call", id: RequestId, params: DynamicToolCallParams, } | { "method": "account/chatgptAuthTokens/refresh", id: RequestId, params: ChatgptAuthTokensRefreshParams, } | { "method": "attestation/generate", id: RequestId, params: AttestationGenerateParams, } | { "method": "applyPatchApproval", id: RequestId, params: ApplyPatchApprovalParams, } | { "method": "execCommandApproval", id: RequestId, params: ExecCommandApprovalParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CurrentTimeReadParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CurrentTimeReadParams.ts deleted file mode 100644 index 80a3e3038731..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CurrentTimeReadParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CurrentTimeReadParams = { threadId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CurrentTimeReadResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CurrentTimeReadResponse.ts deleted file mode 100644 index 4fcdcafa6d8a..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CurrentTimeReadResponse.ts +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type CurrentTimeReadResponse = { -/** - * Current time as whole Unix seconds. - */ -currentTimeAt: number, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index c9b86608f008..2e59b0fe1908 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -90,8 +90,6 @@ export type { ConsumeAccountRateLimitResetCreditParams } from "./ConsumeAccountR export type { ConsumeAccountRateLimitResetCreditResponse } from "./ConsumeAccountRateLimitResetCreditResponse"; export type { ContextCompactedNotification } from "./ContextCompactedNotification"; export type { CreditsSnapshot } from "./CreditsSnapshot"; -export type { CurrentTimeReadParams } from "./CurrentTimeReadParams"; -export type { CurrentTimeReadResponse } from "./CurrentTimeReadResponse"; export type { DeprecationNoticeNotification } from "./DeprecationNoticeNotification"; export type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; export type { DynamicToolCallParams } from "./DynamicToolCallParams"; diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs index 5e6c2ad015af..91eca690f114 100644 --- a/codex-rs/app-server-protocol/src/export.rs +++ b/codex-rs/app-server-protocol/src/export.rs @@ -14,6 +14,9 @@ use crate::export_server_responses; use crate::protocol::common::EXPERIMENTAL_CLIENT_METHOD_PARAM_TYPES; use crate::protocol::common::EXPERIMENTAL_CLIENT_METHOD_RESPONSE_TYPES; use crate::protocol::common::EXPERIMENTAL_CLIENT_METHODS; +use crate::protocol::common::EXPERIMENTAL_SERVER_METHOD_PARAM_TYPES; +use crate::protocol::common::EXPERIMENTAL_SERVER_METHOD_RESPONSE_TYPES; +use crate::protocol::common::EXPERIMENTAL_SERVER_METHODS; use anyhow::Context; use anyhow::Result; use anyhow::anyhow; @@ -249,10 +252,10 @@ fn filter_experimental_ts(out_dir: &Path) -> Result<()> { let registered_fields = experimental_fields(); let experimental_method_types = experimental_method_types(); // Most generated TS files are filtered by schema processing, but - // `ClientRequest.ts` and any type with `#[experimental(...)]` fields need - // direct post-processing because they encode method/field information in - // file-local unions/interfaces. - filter_client_request_ts(out_dir, EXPERIMENTAL_CLIENT_METHODS)?; + // Request unions and types with `#[experimental(...)]` fields need direct + // post-processing because they encode method/field information locally. + filter_request_ts(out_dir, "ClientRequest.ts", EXPERIMENTAL_CLIENT_METHODS)?; + filter_request_ts(out_dir, "ServerRequest.ts", EXPERIMENTAL_SERVER_METHODS)?; filter_experimental_type_fields_ts(out_dir, ®istered_fields)?; remove_generated_type_files(out_dir, &experimental_method_types, "ts")?; Ok(()) @@ -261,10 +264,13 @@ fn filter_experimental_ts(out_dir: &Path) -> Result<()> { pub(crate) fn filter_experimental_ts_tree(tree: &mut BTreeMap) -> Result<()> { let registered_fields = experimental_fields(); let experimental_method_types = experimental_method_types(); - if let Some(content) = tree.get_mut(Path::new("ClientRequest.ts")) { - let filtered = - filter_client_request_ts_contents(std::mem::take(content), EXPERIMENTAL_CLIENT_METHODS); - *content = filtered; + for (file_name, experimental_methods) in [ + ("ClientRequest.ts", EXPERIMENTAL_CLIENT_METHODS), + ("ServerRequest.ts", EXPERIMENTAL_SERVER_METHODS), + ] { + if let Some(content) = tree.get_mut(Path::new(file_name)) { + *content = filter_request_ts_contents(std::mem::take(content), experimental_methods); + } } let mut fields_by_type_name: HashMap> = HashMap::new(); @@ -293,21 +299,21 @@ pub(crate) fn filter_experimental_ts_tree(tree: &mut BTreeMap) Ok(()) } -/// Removes union arms from `ClientRequest.ts` for methods marked experimental. -fn filter_client_request_ts(out_dir: &Path, experimental_methods: &[&str]) -> Result<()> { - let path = out_dir.join("ClientRequest.ts"); +/// Removes union arms from a generated request type for methods marked experimental. +fn filter_request_ts(out_dir: &Path, file_name: &str, experimental_methods: &[&str]) -> Result<()> { + let path = out_dir.join(file_name); if !path.exists() { return Ok(()); } let mut content = fs::read_to_string(&path).with_context(|| format!("Failed to read {}", path.display()))?; - content = filter_client_request_ts_contents(content, experimental_methods); + content = filter_request_ts_contents(content, experimental_methods); fs::write(&path, content).with_context(|| format!("Failed to write {}", path.display()))?; Ok(()) } -fn filter_client_request_ts_contents(mut content: String, experimental_methods: &[&str]) -> String { +fn filter_request_ts_contents(mut content: String, experimental_methods: &[&str]) -> String { let Some((prefix, body, suffix)) = split_type_alias(&content) else { return content; }; @@ -404,6 +410,7 @@ fn filter_experimental_schema(bundle: &mut Value) -> Result<()> { filter_experimental_fields_in_root(bundle, ®istered_fields); filter_experimental_fields_in_definitions(bundle, ®istered_fields); prune_experimental_methods(bundle, EXPERIMENTAL_CLIENT_METHODS); + prune_experimental_methods(bundle, EXPERIMENTAL_SERVER_METHODS); remove_experimental_method_type_definitions(bundle); Ok(()) } @@ -560,6 +567,8 @@ fn experimental_method_types() -> HashSet { collect_experimental_type_names(EXPERIMENTAL_CLIENT_METHOD_PARAM_TYPES, &mut type_names); collect_experimental_type_names(EXPERIMENTAL_CLIENT_METHOD_RESPONSE_TYPES, &mut type_names); collect_experimental_type_names(EXPERIMENTAL_CLIENT_METHOD_DEPENDENCY_TYPES, &mut type_names); + collect_experimental_type_names(EXPERIMENTAL_SERVER_METHOD_PARAM_TYPES, &mut type_names); + collect_experimental_type_names(EXPERIMENTAL_SERVER_METHOD_RESPONSE_TYPES, &mut type_names); type_names } @@ -2118,6 +2127,13 @@ mod tests { client_request_ts.contains("MockExperimentalMethodParams"), false ); + 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")) @@ -2138,6 +2154,14 @@ 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 c49ff8b0f743..e5a882b3b5cc 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1184,7 +1184,8 @@ client_request_definitions! { macro_rules! server_request_definitions { ( $( - $(#[$variant_meta:meta])* + $(#[experimental($reason:expr)])? + $(#[doc = $variant_doc:literal])* $variant:ident $(=> $wire:literal)? { params: $params:ty, response: $response:ty, @@ -1197,7 +1198,7 @@ macro_rules! server_request_definitions { #[serde(tag = "method", rename_all = "camelCase")] pub enum ServerRequest { $( - $(#[$variant_meta])* + $(#[doc = $variant_doc])* $(#[serde(rename = $wire)] #[ts(rename = $wire)])? $variant { #[serde(rename = "id")] @@ -1237,7 +1238,7 @@ macro_rules! server_request_definitions { #[serde(tag = "method", rename_all = "camelCase")] pub enum ServerResponse { $( - $(#[$variant_meta])* + $(#[doc = $variant_doc])* $(#[serde(rename = $wire)])? $variant { #[serde(rename = "id")] @@ -1281,6 +1282,22 @@ macro_rules! server_request_definitions { } } + pub(crate) const EXPERIMENTAL_SERVER_METHODS: &[&str] = &[ + $( + experimental_method_entry!($(#[experimental($reason)])? $(=> $wire)?), + )* + ]; + pub(crate) const EXPERIMENTAL_SERVER_METHOD_PARAM_TYPES: &[&str] = &[ + $( + experimental_type_entry!($(#[experimental($reason)])? $params), + )* + ]; + pub(crate) const EXPERIMENTAL_SERVER_METHOD_RESPONSE_TYPES: &[&str] = &[ + $( + experimental_type_entry!($(#[experimental($reason)])? $response), + )* + ]; + pub fn export_server_responses( out_dir: &::std::path::Path, ) -> ::std::result::Result<(), ::ts_rs::ExportError> { @@ -1470,6 +1487,7 @@ 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, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index f45e93018090..b9c7899fc8fc 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -1471,7 +1471,7 @@ Desktop hosts that provide upstream attestation should set `capabilities.request ### Current time -When `[features.current_time_reminder]` is enabled with `clock_source = "external"`, app-server sends the client subscribed to the thread a `currentTime/read` request with `{ "threadId": "thr_123" }` when a time reminder is due. The client responds with `{ "currentTimeAt": 1781717655 }`, where `currentTimeAt` is an integer Unix timestamp in seconds. A failed, canceled, timed-out, or malformed response stops the turn before the model request is sent. +When `[features.current_time_reminder]` is enabled with `clock_source = "external"`, app-server sends the client subscribed to the thread an experimental `currentTime/read` request with `{ "threadId": "thr_123" }` when a time reminder is due. The client responds with `{ "currentTimeAt": 1781717655 }`, where `currentTimeAt` is an integer Unix timestamp in seconds. A failed, canceled, timed-out, or malformed response stops the turn before the model request is sent. ### MCP server elicitations