From bb98fe0a17ac9c9509f6bd1c1a59f82abf382b16 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Tue, 10 Feb 2026 15:07:13 +0000 Subject: [PATCH 1/4] Add optOutNotificationMethods to app --- .../schema/json/ClientRequest.json | 10 ++ .../codex_app_server_protocol.schemas.json | 10 ++ .../schema/json/v1/InitializeParams.json | 10 ++ .../typescript/InitializeCapabilities.ts | 7 +- .../src/protocol/common.rs | 88 ++++++++++++ .../app-server-protocol/src/protocol/v1.rs | 6 +- codex-rs/app-server-test-client/src/lib.rs | 1 + codex-rs/app-server/README.md | 39 +++++ codex-rs/app-server/src/message_processor.rs | 13 +- codex-rs/app-server/src/transport.rs | 135 +++++++++++++++++- .../app-server/tests/common/mcp_process.rs | 1 + .../tests/suite/v2/experimental_api.rs | 3 + .../app-server/tests/suite/v2/initialize.rs | 70 +++++++++ codex-rs/debug-client/src/client.rs | 1 + 14 files changed, 387 insertions(+), 7 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index a36ebc6a09e..de746e7a78d 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -718,6 +718,16 @@ "default": false, "description": "Opt into receiving experimental API methods and fields.", "type": "boolean" + }, + "optOutNotificationMethods": { + "description": "Exact notification method names that should be suppressed for this connection (for example `codex/event/session_configured`).", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] } }, "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 b7e922dd17a..88fb5ddbdb3 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 @@ -5610,6 +5610,16 @@ "default": false, "description": "Opt into receiving experimental API methods and fields.", "type": "boolean" + }, + "optOutNotificationMethods": { + "description": "Exact notification method names that should be suppressed for this connection (for example `codex/event/session_configured`).", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] } }, "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 dd71c717f1a..050bcb9c506 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/InitializeParams.json @@ -29,6 +29,16 @@ "default": false, "description": "Opt into receiving experimental API methods and fields.", "type": "boolean" + }, + "optOutNotificationMethods": { + "description": "Exact notification method names that should be suppressed for this connection (for example `codex/event/session_configured`).", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] } }, "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 24f53026278..242be57fceb 100644 --- a/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts +++ b/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts @@ -9,4 +9,9 @@ export type InitializeCapabilities = { /** * Opt into receiving experimental API methods and fields. */ -experimentalApi: boolean, }; +experimentalApi: boolean, +/** + * Exact notification method names that should be suppressed for this + * connection (for example `codex/event/session_configured`). + */ +optOutNotificationMethods?: Array, }; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 77d22f4abf3..84523286931 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -806,6 +806,94 @@ mod tests { Ok(()) } + #[test] + fn serialize_initialize_with_opt_out_notification_methods() -> Result<()> { + let request = ClientRequest::Initialize { + request_id: RequestId::Integer(42), + params: v1::InitializeParams { + client_info: v1::ClientInfo { + name: "codex_vscode".to_string(), + title: Some("Codex VS Code Extension".to_string()), + version: "0.1.0".to_string(), + }, + capabilities: Some(v1::InitializeCapabilities { + experimental_api: true, + opt_out_notification_methods: Some(vec![ + "codex/event/session_configured".to_string(), + "item/agentMessage/delta".to_string(), + ]), + }), + }, + }; + + assert_eq!( + json!({ + "method": "initialize", + "id": 42, + "params": { + "clientInfo": { + "name": "codex_vscode", + "title": "Codex VS Code Extension", + "version": "0.1.0" + }, + "capabilities": { + "experimentalApi": true, + "optOutNotificationMethods": [ + "codex/event/session_configured", + "item/agentMessage/delta" + ] + } + } + }), + serde_json::to_value(&request)?, + ); + Ok(()) + } + + #[test] + fn deserialize_initialize_with_opt_out_notification_methods() -> Result<()> { + let request: ClientRequest = serde_json::from_value(json!({ + "method": "initialize", + "id": 42, + "params": { + "clientInfo": { + "name": "codex_vscode", + "title": "Codex VS Code Extension", + "version": "0.1.0" + }, + "capabilities": { + "experimentalApi": true, + "optOutNotificationMethods": [ + "codex/event/session_configured", + "item/agentMessage/delta" + ] + } + } + }))?; + + assert_eq!( + request, + ClientRequest::Initialize { + request_id: RequestId::Integer(42), + params: v1::InitializeParams { + client_info: v1::ClientInfo { + name: "codex_vscode".to_string(), + title: Some("Codex VS Code Extension".to_string()), + version: "0.1.0".to_string(), + }, + capabilities: Some(v1::InitializeCapabilities { + experimental_api: true, + opt_out_notification_methods: Some(vec![ + "codex/event/session_configured".to_string(), + "item/agentMessage/delta".to_string(), + ]), + }), + }, + } + ); + Ok(()) + } + #[test] fn conversation_id_serializes_as_plain_string() -> Result<()> { let id = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?; diff --git a/codex-rs/app-server-protocol/src/protocol/v1.rs b/codex-rs/app-server-protocol/src/protocol/v1.rs index 09b4130b5da..eba81b43898 100644 --- a/codex-rs/app-server-protocol/src/protocol/v1.rs +++ b/codex-rs/app-server-protocol/src/protocol/v1.rs @@ -46,12 +46,16 @@ pub struct ClientInfo { } /// Client-declared capabilities negotiated during initialize. -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, JsonSchema, TS)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct InitializeCapabilities { /// Opt into receiving experimental API methods and fields. #[serde(default)] pub experimental_api: bool, + /// Exact notification method names that should be suppressed for this + /// connection (for example `codex/event/session_configured`). + #[ts(optional)] + pub opt_out_notification_methods: Option>, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] diff --git a/codex-rs/app-server-test-client/src/lib.rs b/codex-rs/app-server-test-client/src/lib.rs index 90f6adf572f..415f45e4a19 100644 --- a/codex-rs/app-server-test-client/src/lib.rs +++ b/codex-rs/app-server-test-client/src/lib.rs @@ -511,6 +511,7 @@ impl CodexClient { }, capabilities: Some(InitializeCapabilities { experimental_api: true, + opt_out_notification_methods: None, }), }, }; diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 66d4a501ec5..90b37c9b66d 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -59,6 +59,8 @@ Use the thread APIs to create, list, or archive conversations. Drive a conversat Clients must send a single `initialize` request per transport connection before invoking any other method on that connection, then acknowledge with an `initialized` notification. The server returns the user agent string it will present to upstream services; subsequent requests issued before initialization receive a `"Not initialized"` error, and repeated `initialize` calls on the same connection receive an `"Already initialized"` error. +`initialize.params.capabilities` also supports per-connection notification opt-out via `optOutNotificationMethods`, which is a list of exact method names to suppress for that connection. Matching is exact (no wildcards/prefixes). Unknown method names are accepted and ignored. + Applications building on top of `codex app-server` should identify themselves via the `clientInfo` parameter. **Important**: `clientInfo.name` is used to identify the client for the OpenAI Compliance Logs Platform. If @@ -81,6 +83,29 @@ Example (from OpenAI's official VSCode extension): } ``` +Example with notification opt-out: + +```json +{ + "method": "initialize", + "id": 1, + "params": { + "clientInfo": { + "name": "my_client", + "title": "My Client", + "version": "0.1.0" + }, + "capabilities": { + "experimentalApi": true, + "optOutNotificationMethods": [ + "codex/event/session_configured", + "item/agentMessage/delta" + ] + } + } +} +``` + ## API Overview - `thread/start` — create a new thread; emits `thread/started` and auto-subscribes you to turn/item events for that thread. @@ -487,6 +512,20 @@ Notes: Event notifications are the server-initiated event stream for thread lifecycles, turn lifecycles, and the items within them. After you start or resume a thread, keep reading stdout for `thread/started`, `turn/*`, and `item/*` notifications. +### Notification opt-out + +Clients can suppress specific notifications per connection by sending exact method names in `initialize.params.capabilities.optOutNotificationMethods`. + +- Exact-match only: `item/agentMessage/delta` suppresses only that method. +- Unknown method names are ignored. +- Applies to both legacy (`codex/event/*`) and v2 (`thread/*`, `turn/*`, `item/*`, etc.) notifications. +- Does not apply to requests/responses/errors. + +Examples: + +- Opt out of legacy session setup event: `codex/event/session_configured` +- Opt out of streamed agent text deltas: `item/agentMessage/delta` + ### Turn events The app-server streams JSON-RPC notifications while a turn is running. Each turn starts with `turn/started` (initial `turn`) and ends with `turn/completed` (final `turn` status). Token usage events stream separately via `thread/tokenUsage/updated`. Clients subscribe to the events they care about, rendering each item incrementally as updates arrive. The per-item lifecycle is always: `item/started` → zero or more item-specific deltas → `item/completed`. diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 26da44df311..c54af4095bb 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::path::PathBuf; use std::sync::Arc; use std::sync::RwLock; @@ -117,6 +118,7 @@ pub(crate) struct MessageProcessor { #[derive(Debug, Default)] pub(crate) struct ConnectionSessionState { pub(crate) initialized: bool, + pub(crate) opted_out_notification_methods: HashSet, experimental_api_enabled: bool, } @@ -239,16 +241,19 @@ impl MessageProcessor { self.outgoing.send_error(request_id, error).await; return; } else { + let capabilities = params.capabilities.unwrap_or_default(); // TODO(maxj): Revisit capability scoping for `experimental_api_enabled`. // Current behavior is per-connection. Reviewer feedback notes this can // create odd cross-client behavior (for example dynamic tool calls on a // shared thread when another connected client did not opt into // experimental API). Proposed direction is instance-global first-write-wins // with initialize-time mismatch rejection. - session.experimental_api_enabled = params - .capabilities - .as_ref() - .is_some_and(|cap| cap.experimental_api); + session.experimental_api_enabled = capabilities.experimental_api; + session.opted_out_notification_methods = capabilities + .opt_out_notification_methods + .unwrap_or_default() + .into_iter() + .collect(); let ClientInfo { name, title: _title, diff --git a/codex-rs/app-server/src/transport.rs b/codex-rs/app-server/src/transport.rs index 39fd13212cf..c439e5df616 100644 --- a/codex-rs/app-server/src/transport.rs +++ b/codex-rs/app-server/src/transport.rs @@ -382,10 +382,23 @@ pub(crate) async fn route_outgoing_envelope( } } OutgoingEnvelope::Broadcast { message } => { + let notification_method = match &message { + OutgoingMessage::Notification(notification) => Some(notification.method.clone()), + OutgoingMessage::AppServerNotification(notification) => { + Some(notification.to_string()) + } + _ => None, + }; let target_connections: Vec = connections .iter() .filter_map(|(connection_id, connection_state)| { - if connection_state.session.initialized { + let is_opted_out = notification_method.as_ref().is_some_and(|method| { + connection_state + .session + .opted_out_notification_methods + .contains(method) + }); + if connection_state.session.initialized && !is_opted_out { Some(*connection_id) } else { None @@ -416,7 +429,15 @@ pub(crate) fn has_initialized_connections( #[cfg(test)] mod tests { use super::*; + use crate::outgoing_message::OutgoingNotification; + use crate::outgoing_message::OutgoingResponse; + use codex_app_server_protocol::DeprecationNoticeNotification; + use codex_app_server_protocol::RequestId; + use codex_app_server_protocol::ServerNotification; use pretty_assertions::assert_eq; + use serde_json::json; + use tokio::time::Duration; + use tokio::time::timeout; #[test] fn app_server_transport_parses_stdio_listen_url() { @@ -456,4 +477,116 @@ mod tests { "unsupported --listen URL `http://127.0.0.1:1234`; expected `stdio://` or `ws://IP:PORT`" ); } + + fn initialized_connection_state( + opted_out_notification_methods: &[&str], + ) -> (ConnectionState, mpsc::Receiver) { + let (writer, reader) = mpsc::channel(CHANNEL_CAPACITY); + let mut connection_state = ConnectionState::new(writer); + connection_state.session.initialized = true; + connection_state.session.opted_out_notification_methods = opted_out_notification_methods + .iter() + .map(|method| (*method).to_string()) + .collect(); + (connection_state, reader) + } + + #[tokio::test] + async fn route_outgoing_envelope_broadcast_sends_notification_when_not_opted_out() { + let (connection_state, mut reader) = initialized_connection_state(&[]); + let mut connections = HashMap::from([(ConnectionId(1), connection_state)]); + + route_outgoing_envelope( + &mut connections, + OutgoingEnvelope::Broadcast { + message: OutgoingMessage::Notification(OutgoingNotification { + method: "codex/event/session_configured".to_string(), + params: None, + }), + }, + ) + .await; + + let message = timeout(Duration::from_millis(100), reader.recv()) + .await + .expect("expected notification to be sent") + .expect("connection should still be open"); + let OutgoingMessage::Notification(notification) = message else { + panic!("expected legacy notification message"); + }; + assert_eq!(notification.method, "codex/event/session_configured"); + } + + #[tokio::test] + async fn route_outgoing_envelope_broadcast_suppresses_opted_out_legacy_notification() { + let (connection_state, mut reader) = + initialized_connection_state(&["codex/event/session_configured"]); + let mut connections = HashMap::from([(ConnectionId(1), connection_state)]); + + route_outgoing_envelope( + &mut connections, + OutgoingEnvelope::Broadcast { + message: OutgoingMessage::Notification(OutgoingNotification { + method: "codex/event/session_configured".to_string(), + params: None, + }), + }, + ) + .await; + + timeout(Duration::from_millis(100), reader.recv()) + .await + .expect_err("notification should be filtered"); + } + + #[tokio::test] + async fn route_outgoing_envelope_broadcast_suppresses_opted_out_v2_notification() { + let (connection_state, mut reader) = initialized_connection_state(&["deprecationNotice"]); + let mut connections = HashMap::from([(ConnectionId(1), connection_state)]); + + route_outgoing_envelope( + &mut connections, + OutgoingEnvelope::Broadcast { + message: OutgoingMessage::AppServerNotification( + ServerNotification::DeprecationNotice(DeprecationNoticeNotification { + summary: "deprecated".to_string(), + details: None, + }), + ), + }, + ) + .await; + + timeout(Duration::from_millis(100), reader.recv()) + .await + .expect_err("notification should be filtered"); + } + + #[tokio::test] + async fn route_outgoing_envelope_broadcast_does_not_filter_non_notification_messages() { + let (connection_state, mut reader) = + initialized_connection_state(&["account/updated", "codex/event/task_complete"]); + let mut connections = HashMap::from([(ConnectionId(1), connection_state)]); + + route_outgoing_envelope( + &mut connections, + OutgoingEnvelope::Broadcast { + message: OutgoingMessage::Response(OutgoingResponse { + id: RequestId::Integer(7), + result: json!({"ok": true}), + }), + }, + ) + .await; + + let message = timeout(Duration::from_millis(100), reader.recv()) + .await + .expect("expected message to be sent") + .expect("connection should still be open"); + let OutgoingMessage::Response(response) = message else { + panic!("expected response message"); + }; + assert_eq!(response.id, RequestId::Integer(7)); + assert_eq!(response.result, json!({"ok": true})); + } } diff --git a/codex-rs/app-server/tests/common/mcp_process.rs b/codex-rs/app-server/tests/common/mcp_process.rs index c1d004d4071..7f77d8fc92a 100644 --- a/codex-rs/app-server/tests/common/mcp_process.rs +++ b/codex-rs/app-server/tests/common/mcp_process.rs @@ -174,6 +174,7 @@ impl McpProcess { client_info, Some(InitializeCapabilities { experimental_api: true, + opt_out_notification_methods: None, }), ) .await 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 5116633a480..798d52abfd0 100644 --- a/codex-rs/app-server/tests/suite/v2/experimental_api.rs +++ b/codex-rs/app-server/tests/suite/v2/experimental_api.rs @@ -30,6 +30,7 @@ async fn mock_experimental_method_requires_experimental_api_capability() -> Resu default_client_info(), Some(InitializeCapabilities { experimental_api: false, + opt_out_notification_methods: None, }), ) .await?; @@ -61,6 +62,7 @@ async fn thread_start_mock_field_requires_experimental_api_capability() -> Resul default_client_info(), Some(InitializeCapabilities { experimental_api: false, + opt_out_notification_methods: None, }), ) .await?; @@ -97,6 +99,7 @@ async fn thread_start_without_dynamic_tools_allows_without_experimental_api_capa default_client_info(), Some(InitializeCapabilities { experimental_api: false, + opt_out_notification_methods: None, }), ) .await?; diff --git a/codex-rs/app-server/tests/suite/v2/initialize.rs b/codex-rs/app-server/tests/suite/v2/initialize.rs index b31a68833f3..2edc83a7f49 100644 --- a/codex-rs/app-server/tests/suite/v2/initialize.rs +++ b/codex-rs/app-server/tests/suite/v2/initialize.rs @@ -3,8 +3,12 @@ use app_test_support::McpProcess; use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::to_response; use codex_app_server_protocol::ClientInfo; +use codex_app_server_protocol::InitializeCapabilities; use codex_app_server_protocol::InitializeResponse; use codex_app_server_protocol::JSONRPCMessage; +use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::ThreadStartParams; +use codex_app_server_protocol::ThreadStartResponse; use pretty_assertions::assert_eq; use std::path::Path; use tempfile::TempDir; @@ -108,6 +112,72 @@ async fn initialize_rejects_invalid_client_name() -> Result<()> { Ok(()) } +#[tokio::test] +async fn initialize_opt_out_notification_methods_filters_notifications() -> Result<()> { + let responses = Vec::new(); + let server = create_mock_responses_server_sequence_unchecked(responses).await; + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri(), "never")?; + let mut mcp = McpProcess::new(codex_home.path()).await?; + + let message = timeout( + DEFAULT_READ_TIMEOUT, + mcp.initialize_with_capabilities( + ClientInfo { + name: "codex_vscode".to_string(), + title: Some("Codex VS Code Extension".to_string()), + version: "0.1.0".to_string(), + }, + Some(InitializeCapabilities { + experimental_api: true, + opt_out_notification_methods: Some(vec![ + "thread/started".to_string(), + "codex/event/session_configured".to_string(), + ]), + }), + ), + ) + .await??; + let JSONRPCMessage::Response(_) = message else { + anyhow::bail!("expected initialize response, got {message:?}"); + }; + + let request_id = mcp + .send_thread_start_request(ThreadStartParams::default()) + .await?; + let response = timeout(DEFAULT_READ_TIMEOUT, async { + loop { + let message = mcp.read_next_message().await?; + match message { + JSONRPCMessage::Response(response) + if response.id == RequestId::Integer(request_id) => + { + return Ok(response); + } + JSONRPCMessage::Notification(notification) + if notification.method == "thread/started" => + { + anyhow::bail!("thread/started should be filtered by optOutNotificationMethods"); + } + _ => {} + } + } + }) + .await??; + let _: ThreadStartResponse = to_response(response)?; + + let thread_started = timeout( + std::time::Duration::from_millis(500), + mcp.read_stream_until_notification_message("thread/started"), + ) + .await; + assert!( + thread_started.is_err(), + "thread/started should be filtered by optOutNotificationMethods" + ); + Ok(()) +} + // Helper to create a config.toml pointing at the mock model server. fn create_config_toml( codex_home: &Path, diff --git a/codex-rs/debug-client/src/client.rs b/codex-rs/debug-client/src/client.rs index cf54ef9855c..cacb5a77326 100644 --- a/codex-rs/debug-client/src/client.rs +++ b/codex-rs/debug-client/src/client.rs @@ -102,6 +102,7 @@ impl AppServerClient { }, capabilities: Some(InitializeCapabilities { experimental_api: true, + opt_out_notification_methods: None, }), }, }; From 15dc7c6dac14160d149765ed7e4d9dc6cd00c770 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Tue, 10 Feb 2026 16:41:57 +0000 Subject: [PATCH 2/4] Update v1.rs --- codex-rs/app-server-protocol/src/protocol/v1.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/app-server-protocol/src/protocol/v1.rs b/codex-rs/app-server-protocol/src/protocol/v1.rs index eba81b43898..25e60ca7516 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 { pub experimental_api: bool, /// Exact notification method names that should be suppressed for this /// connection (for example `codex/event/session_configured`). - #[ts(optional)] + #[ts(optional = nullable)] pub opt_out_notification_methods: Option>, } From 4f00f4799e6ce2bf4fc56545306d9b57780d088e Mon Sep 17 00:00:00 2001 From: jif-oai Date: Tue, 10 Feb 2026 17:16:32 +0000 Subject: [PATCH 3/4] fix --- .../schema/typescript/InitializeCapabilities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts b/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts index 242be57fceb..a6ac24efcdf 100644 --- a/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts +++ b/codex-rs/app-server-protocol/schema/typescript/InitializeCapabilities.ts @@ -14,4 +14,4 @@ experimentalApi: boolean, * Exact notification method names that should be suppressed for this * connection (for example `codex/event/session_configured`). */ -optOutNotificationMethods?: Array, }; +optOutNotificationMethods?: Array | null, }; From 7a713f56937708df02c5af4c80d1ba791a570190 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Tue, 10 Feb 2026 17:29:34 +0000 Subject: [PATCH 4/4] fix --- codex-rs/app-server-protocol/src/export.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs index 5c4954b3cc0..2cb3f9d1f1c 100644 --- a/codex-rs/app-server-protocol/src/export.rs +++ b/codex-rs/app-server-protocol/src/export.rs @@ -1474,7 +1474,9 @@ mod tests { let allow_optional_nullable = path .file_stem() .and_then(|stem| stem.to_str()) - .is_some_and(|stem| stem.ends_with("Params")); + .is_some_and(|stem| { + stem.ends_with("Params") || stem == "InitializeCapabilities" + }); let contents = fs::read_to_string(&path)?; if contents.contains("| undefined") {