From 9121132c8f5412ae99c36363409759baa7e004f9 Mon Sep 17 00:00:00 2001 From: alexsong-oai Date: Thu, 30 Apr 2026 13:03:21 -0700 Subject: [PATCH] Send external import completion for sync imports (#20379) --- codex-rs/app-server/README.md | 2 +- codex-rs/app-server/src/message_processor.rs | 13 +++---- .../tests/suite/v2/external_agent_config.rs | 39 +++++++++++++++++++ 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index c001cef99..b986a8545 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -222,7 +222,7 @@ Example with notification opt-out: - `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); returns the tracking thread id. - `config/read` — fetch the effective config on disk after resolving config layering. - `externalAgentConfig/detect` — detect migratable external-agent artifacts with `includeHome` and optional `cwds`; each detected item includes `cwd` (`null` for home), and plugin/session migration items may additionally include structured `details` grouping plugin ids or session metadata. -- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any plugin/session `details` returned by detect. When a request includes plugin or session imports, the server emits `externalAgentConfig/import/completed` after the full import finishes (immediately after the response when everything completed synchronously, or after background imports finish). +- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any plugin/session `details` returned by detect. When a request includes migration items, the server emits `externalAgentConfig/import/completed` once after the full import finishes (immediately after the response when everything completed synchronously, or after background imports finish). - `config/value/write` — write a single config key/value to the user's config.toml on disk. - `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads. - `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index aec4ec509..b79bf7732 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -1212,18 +1212,13 @@ impl MessageProcessor { params: ExternalAgentConfigImportParams, ) -> Result<(), JSONRPCErrorError> { let needs_runtime_refresh = migration_items_need_runtime_refresh(¶ms.migration_items); + let has_migration_items = !params.migration_items.is_empty(); let has_plugin_imports = params.migration_items.iter().any(|item| { matches!( item.item_type, ExternalAgentConfigMigrationItemType::Plugins ) }); - let has_session_imports = params.migration_items.iter().any(|item| { - matches!( - item.item_type, - ExternalAgentConfigMigrationItemType::Sessions - ) - }); let pending_session_imports = self .external_agent_config_api .validate_pending_session_imports(¶ms)?; @@ -1235,11 +1230,13 @@ impl MessageProcessor { .send_response(request_id, ExternalAgentConfigImportResponse {}) .await; - if !has_plugin_imports && !has_session_imports { + if !has_migration_items { return Ok(()); } - if pending_plugin_imports.is_empty() && pending_session_imports.is_empty() { + let has_background_imports = + !pending_plugin_imports.is_empty() || !pending_session_imports.is_empty(); + if !has_background_imports { self.outgoing .send_server_notification(ServerNotification::ExternalAgentConfigImportCompleted( ExternalAgentConfigImportCompletedNotification {}, diff --git a/codex-rs/app-server/tests/suite/v2/external_agent_config.rs b/codex-rs/app-server/tests/suite/v2/external_agent_config.rs index f160de1a7..e63aad9da 100644 --- a/codex-rs/app-server/tests/suite/v2/external_agent_config.rs +++ b/codex-rs/app-server/tests/suite/v2/external_agent_config.rs @@ -32,6 +32,45 @@ use tokio::time::timeout; const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60); +#[tokio::test] +async fn external_agent_config_import_sends_completion_notification_for_sync_only_import() +-> Result<()> { + let codex_home = TempDir::new()?; + let home_dir = codex_home.path().display().to_string(); + let mut mcp = + McpProcess::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_raw_request( + "externalAgentConfig/import", + Some(serde_json::json!({ + "migrationItems": [{ + "itemType": "CONFIG", + "description": "Import config", + "cwd": null + }] + })), + ) + .await?; + + let response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + let response: ExternalAgentConfigImportResponse = to_response(response)?; + assert_eq!(response, ExternalAgentConfigImportResponse {}); + let notification = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_notification_message("externalAgentConfig/import/completed"), + ) + .await??; + assert_eq!(notification.method, "externalAgentConfig/import/completed"); + + Ok(()) +} + #[tokio::test] async fn external_agent_config_import_sends_completion_notification_for_local_plugins() -> Result<()> {