From 50ef7395faee1d0e2d01730f9636aa06091c7be3 Mon Sep 17 00:00:00 2001 From: charlesgong-openai Date: Sun, 9 Aug 2026 18:37:06 +0000 Subject: [PATCH] Report I/O subtypes for session config import failures (#37723) ## What changed - Append a stable `std::io::ErrorKind` category to the `failed_to_load_session_config` subtype, including categories such as `invalid_data`, `not_found`, and `permission_denied`. - Propagate the categorized subtype through session import failure reporting. ## Testing - Add an app-server test that verifies an invalid config reports `failed_to_load_session_config_invalid_data` in both the import completion notification and analytics event. GitOrigin-RevId: e9a1a7cd36979911f422805180dd91b6ed65e14e --- .../session_importer.rs | 30 ++++- .../tests/suite/v2/external_agent_config.rs | 105 ++++++++++++++++++ 2 files changed, 129 insertions(+), 6 deletions(-) diff --git a/codex-rs/app-server/src/external_agent_migration/session_importer.rs b/codex-rs/app-server/src/external_agent_migration/session_importer.rs index 88e1a9ad16..07ff5cf7c4 100644 --- a/codex-rs/app-server/src/external_agent_migration/session_importer.rs +++ b/codex-rs/app-server/src/external_agent_migration/session_importer.rs @@ -1,5 +1,6 @@ use std::collections::BTreeMap; use std::collections::BTreeSet; +use std::io::ErrorKind; use std::path::PathBuf; use std::sync::Arc; @@ -170,7 +171,7 @@ impl ExternalAgentSessionImporter { record_import_error( &mut item_result, stage, - Some(sub_error_type), + Some(sub_error_type.as_str()), message, Some(source_path.display().to_string()), ); @@ -408,8 +409,25 @@ impl ExternalAgentSessionImporter { ) .await .map_err(|err| { + let io_kind = match err.kind() { + ErrorKind::NotFound => "not_found", + ErrorKind::PermissionDenied => "permission_denied", + ErrorKind::AlreadyExists => "already_exists", + ErrorKind::InvalidInput => "invalid_input", + ErrorKind::InvalidData => "invalid_data", + ErrorKind::IsADirectory => "is_a_directory", + ErrorKind::NotADirectory => "not_a_directory", + ErrorKind::TimedOut => "timed_out", + ErrorKind::WriteZero => "write_zero", + ErrorKind::UnexpectedEof => "unexpected_eof", + ErrorKind::StorageFull => "storage_full", + ErrorKind::QuotaExceeded => "quota_exceeded", + ErrorKind::FileTooLarge => "file_too_large", + ErrorKind::ReadOnlyFilesystem => "read_only_filesystem", + _ => "other", + }; SessionImportStepFailure::new( - "failed_to_load_session_config", + format!("failed_to_load_session_config_{io_kind}"), format!("failed to load imported session config: {err}"), ) })?; @@ -583,18 +601,18 @@ struct SessionImportFailure { source_path: PathBuf, message: String, stage: &'static str, - sub_error_type: &'static str, + sub_error_type: String, } struct SessionImportStepFailure { - sub_error_type: &'static str, + sub_error_type: String, message: String, } impl SessionImportStepFailure { - fn new(sub_error_type: &'static str, message: String) -> Self { + fn new(sub_error_type: impl Into, message: String) -> Self { Self { - sub_error_type, + sub_error_type: sub_error_type.into(), message, } } 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 8f4e949017..edbe6f973d 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 @@ -1379,6 +1379,111 @@ async fn external_agent_config_import_completed_tracks_analytics_event() -> Resu Ok(()) } +#[tokio::test] +async fn external_agent_config_import_reports_session_config_error_subtype() -> Result<()> { + let analytics_server = start_analytics_events_server().await?; + let codex_home = TempDir::new()?; + write_analytics_config(codex_home.path(), &analytics_server.uri())?; + write_chatgpt_auth( + codex_home.path(), + ChatGptAuthFixture::new("chatgpt-token") + .account_id("account-123") + .chatgpt_user_id("user-123") + .chatgpt_account_id("account-123"), + AuthCredentialsStoreMode::File, + )?; + + let project_root = codex_home.path().join("repo"); + let session_dir = external_agent_home(codex_home.path()).join("projects/repo"); + let session_path = session_dir.join("session.jsonl"); + let recent_timestamp = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true); + std::fs::create_dir_all(&project_root)?; + std::fs::create_dir_all(&session_dir)?; + std::fs::write( + &session_path, + serde_json::json!({ + "type": "user", + "cwd": &project_root, + "timestamp": &recent_timestamp, + "message": { "content": "first request" }, + }) + .to_string(), + )?; + + let home_dir = codex_home.path().display().to_string(); + let mut mcp = TestAppServer::builder() + .with_codex_home(codex_home.path()) + .without_auto_env() + .with_env_overrides(&[("HOME", Some(home_dir.as_str()))]) + .build_initialized_with_timeout(DEFAULT_TIMEOUT) + .await?; + std::fs::write( + codex_home.path().join("config.toml"), + "chatgpt_base_url = [", + )?; + + let request_id = mcp + .send_raw_request( + "externalAgentConfig/import", + Some(serde_json::json!({ + "source": "test_import", + "providerId": "test-provider-42", + "migrationItems": [{ + "itemType": "SESSIONS", + "description": "Migrate recent sessions", + "cwd": null, + "details": { + "sessions": [{ + "path": session_path, + "cwd": project_root, + "title": "first request" + }] + } + }] + })), + ) + .await?; + let response: ExternalAgentConfigImportResponse = + timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??; + let import_id = assert_import_response(response); + let completed: ExternalAgentConfigImportCompletedNotification = timeout( + DEFAULT_TIMEOUT, + mcp.read_notification("externalAgentConfig/import/completed"), + ) + .await??; + assert_eq!(completed.import_id, import_id); + assert_eq!(completed.item_type_results.len(), 1); + assert_eq!(completed.item_type_results[0].successes.len(), 0); + assert_eq!(completed.item_type_results[0].failures.len(), 1); + let failure = &completed.item_type_results[0].failures[0]; + assert_eq!(failure.failure_stage, "session_persist"); + assert_eq!( + failure.sub_error_type.as_deref(), + Some("failed_to_load_session_config_invalid_data") + ); + + let event = wait_for_analytics_event( + &analytics_server, + DEFAULT_TIMEOUT, + "codex_onboarding_external_agent_import_failure", + ) + .await?; + let event_params = &event["event_params"]; + assert_eq!(event_params["import_id"], serde_json::json!(import_id)); + assert_eq!(event_params["source"], "test_import"); + assert_eq!(event_params["provider_id"], "test-provider-42"); + assert_eq!(event_params["type"], "SESSIONS"); + assert_eq!(event_params["failure_stage"], "session_persist"); + assert_eq!( + event_params["sub_error_type"], + "failed_to_load_session_config_invalid_data" + ); + assert!(event_params.get("raw_errors").is_none()); + assert!(event_params.get("message").is_none()); + + Ok(()) +} + #[tokio::test] async fn external_agent_config_import_reinstalls_plugins_from_known_marketplaces() -> Result<()> { let codex_home = TempDir::new()?;