From e428a12d2235fe2bc10b10bc45d245d1f491f3c7 Mon Sep 17 00:00:00 2001 From: xl-openai Date: Sun, 28 Jun 2026 11:46:25 -0700 Subject: [PATCH 1/7] [codex] Enable remote plugins by default (#30297) ## Summary - enable the remote plugin feature by default - promote the remote plugin feature from under development to stable - preserve the existing `features.remote_plugin` override for explicitly disabling it - keep legacy disabled-path coverage explicit in TUI and app-server tests ## Impact Remote plugin functionality is enabled by default for configurations that do not set the feature flag. The existing Codex backend authentication gate still applies. ## Validation - `just fmt` - `just test -p codex-features` - `just test -p codex-tui plugins_popup_remote_section_fallback_states_snapshot` - targeted `codex-app-server` plugin-list and skills-list tests - `git diff --check` The full TUI and app-server suites were also exercised locally. All remote-plugin-related coverage passed; unrelated local sandbox/test-binary failures remain outside this change. --- .../src/plugin_analytics_mutation_smoke.rs | 2 - .../src/plugin_analytics_smoke.rs | 1 - .../tests/suite/v2/plugin_install.rs | 2 - .../app-server/tests/suite/v2/plugin_list.rs | 47 +++++++++++++------ .../app-server/tests/suite/v2/plugin_read.rs | 1 - .../app-server/tests/suite/v2/plugin_share.rs | 3 -- .../tests/suite/v2/plugin_uninstall.rs | 1 - .../tests/suite/v2/recommended_plugins.rs | 4 +- .../app-server/tests/suite/v2/skills_list.rs | 33 ++++++------- .../core-plugins/src/discoverable_tests.rs | 15 +++--- codex-rs/core-plugins/src/manager_tests.rs | 8 ---- codex-rs/core-plugins/src/test_support.rs | 2 +- .../core/src/plugins/discoverable_tests.rs | 2 - codex-rs/features/src/lib.rs | 6 +-- .../chatwidget/tests/popups_and_settings.rs | 4 +- 15 files changed, 62 insertions(+), 69 deletions(-) diff --git a/codex-rs/app-server-test-client/src/plugin_analytics_mutation_smoke.rs b/codex-rs/app-server-test-client/src/plugin_analytics_mutation_smoke.rs index 70aa13ab9c..c9f9653155 100644 --- a/codex-rs/app-server-test-client/src/plugin_analytics_mutation_smoke.rs +++ b/codex-rs/app-server-test-client/src/plugin_analytics_mutation_smoke.rs @@ -124,7 +124,6 @@ pub(super) fn run_cleanup( overrides.extend([ "analytics.enabled=false".to_string(), "features.plugins=true".to_string(), - "features.remote_plugin=true".to_string(), ]); let mut client = CodexClient::spawn_stdio(codex_bin, &overrides)?; client.initialize()?; @@ -199,7 +198,6 @@ fn spawn_client( overrides.extend([ "analytics.enabled=true".to_string(), "features.plugins=true".to_string(), - "features.remote_plugin=true".to_string(), ]); let environment = vec![( OsString::from(ANALYTICS_CAPTURE_ENV_VAR), diff --git a/codex-rs/app-server-test-client/src/plugin_analytics_smoke.rs b/codex-rs/app-server-test-client/src/plugin_analytics_smoke.rs index 65704eeb65..aaf54dd2da 100644 --- a/codex-rs/app-server-test-client/src/plugin_analytics_smoke.rs +++ b/codex-rs/app-server-test-client/src/plugin_analytics_smoke.rs @@ -263,7 +263,6 @@ fn smoke_config_overrides(responses_base_url: &str) -> Result> { Ok(vec![ "analytics.enabled=true".to_string(), "features.plugins=true".to_string(), - "features.remote_plugin=true".to_string(), format!("model={}", quoted(MOCK_MODEL_SLUG)?), format!("model_provider={}", quoted(MOCK_PROVIDER_ID)?), format!( diff --git a/codex-rs/app-server/tests/suite/v2/plugin_install.rs b/codex-rs/app-server/tests/suite/v2/plugin_install.rs index 34fc9b18a4..975338e11d 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_install.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_install.rs @@ -2027,7 +2027,6 @@ chatgpt_base_url = "{base_url}" [features] plugins = true -remote_plugin = true "# ), ) @@ -2057,7 +2056,6 @@ chatgpt_base_url = "{}/backend-api/" [features] plugins = true -remote_plugin = true connectors = true "#, server.uri() diff --git a/codex-rs/app-server/tests/suite/v2/plugin_list.rs b/codex-rs/app-server/tests/suite/v2/plugin_list.rs index a301714cf7..974bb8a2c5 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -71,6 +71,23 @@ plugins = true ) } +fn write_remote_plugins_disabled_config_with_base_url( + codex_home: &std::path::Path, + base_url: &str, +) -> std::io::Result<()> { + std::fs::write( + codex_home.join("config.toml"), + format!( + r#"chatgpt_base_url = "{base_url}" + +[features] +plugins = true +remote_plugin = false +"#, + ), + ) +} + #[tokio::test] async fn plugin_list_skips_invalid_marketplace_file_and_reports_error() -> Result<()> { let codex_home = TempDir::new()?; @@ -197,7 +214,6 @@ async fn plugin_installed_prefers_remote_curated_conflicts_when_remote_plugin_en [features] plugins = true -remote_plugin = true plugin_sharing = false [plugins."linear@openai-curated"] @@ -1987,10 +2003,11 @@ async fn plugin_list_uses_cached_global_remote_catalog_and_refreshes_it() -> Res } #[tokio::test] -async fn plugin_list_includes_openai_curated_remote_collection_when_requested() -> Result<()> { +async fn plugin_list_includes_openai_curated_remote_collection_when_remote_plugin_disabled_and_requested() +-> Result<()> { let codex_home = TempDir::new()?; let server = MockServer::start().await; - write_plugins_enabled_config_with_base_url( + write_remote_plugins_disabled_config_with_base_url( codex_home.path(), &format!("{}/backend-api/", server.uri()), )?; @@ -2092,10 +2109,11 @@ async fn plugin_list_includes_openai_curated_remote_collection_when_requested() } #[tokio::test] -async fn plugin_list_propagates_explicit_openai_curated_remote_collection_errors() -> Result<()> { +async fn plugin_list_propagates_openai_curated_remote_collection_errors_when_remote_plugin_disabled() +-> Result<()> { let codex_home = TempDir::new()?; let server = MockServer::start().await; - write_plugins_enabled_config_with_base_url( + write_remote_plugins_disabled_config_with_base_url( codex_home.path(), &format!("{}/backend-api/", server.uri()), )?; @@ -2148,10 +2166,11 @@ async fn plugin_list_propagates_explicit_openai_curated_remote_collection_errors } #[tokio::test] -async fn plugin_list_skips_explicit_openai_curated_remote_collection_for_api_auth() -> Result<()> { +async fn plugin_list_skips_openai_curated_remote_collection_for_api_auth_when_remote_plugin_disabled() +-> Result<()> { let codex_home = TempDir::new()?; let server = MockServer::start().await; - write_plugins_enabled_config_with_base_url( + write_remote_plugins_disabled_config_with_base_url( codex_home.path(), &format!("{}/backend-api/", server.uri()), )?; @@ -2391,7 +2410,8 @@ async fn plugin_list_does_not_append_global_remote_when_marketplace_kinds_are_ex } #[tokio::test] -async fn plugin_installed_includes_remote_shared_with_me_plugins() -> Result<()> { +async fn plugin_installed_includes_remote_shared_with_me_plugins_when_remote_plugin_disabled() +-> Result<()> { let codex_home = TempDir::new()?; let server = MockServer::start().await; std::fs::write( @@ -2493,7 +2513,8 @@ plugin_sharing = true } #[tokio::test] -async fn plugin_installed_includes_workspace_directory_without_plugin_sharing() -> Result<()> { +async fn plugin_installed_includes_workspace_directory_without_plugin_sharing_when_remote_plugin_disabled() +-> Result<()> { let codex_home = TempDir::new()?; let server = MockServer::start().await; std::fs::write( @@ -2590,7 +2611,6 @@ async fn plugin_installed_includes_created_by_me_when_remote_plugins_enabled() - [features] plugins = true -remote_plugin = true plugin_sharing = false "#, server.uri() @@ -2681,7 +2701,6 @@ async fn plugin_installed_starts_remote_installed_bundle_sync() -> Result<()> { [features] plugins = true -remote_plugin = true plugin_sharing = false "#, server.uri() @@ -2750,10 +2769,10 @@ plugin_sharing = false } #[tokio::test] -async fn plugin_list_fetches_workspace_directory_kind_without_remote_plugin_flag() -> Result<()> { +async fn plugin_list_fetches_workspace_directory_kind_when_remote_plugin_disabled() -> Result<()> { let codex_home = TempDir::new()?; let server = MockServer::start().await; - write_plugins_enabled_config_with_base_url( + write_remote_plugins_disabled_config_with_base_url( codex_home.path(), &format!("{}/backend-api/", server.uri()), )?; @@ -2848,7 +2867,6 @@ async fn plugin_list_fetches_user_plugins_in_created_by_me_remote_marketplace() [features] plugins = true -remote_plugin = true plugin_sharing = false "#, server.uri() @@ -4032,7 +4050,6 @@ chatgpt_base_url = "{base_url}" [features] plugins = true -remote_plugin = true "# ), ) diff --git a/codex-rs/app-server/tests/suite/v2/plugin_read.rs b/codex-rs/app-server/tests/suite/v2/plugin_read.rs index 011cbec52e..41b7990ec8 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_read.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_read.rs @@ -2132,7 +2132,6 @@ chatgpt_base_url = "{base_url}" [features] plugins = true -remote_plugin = true "# ), ) diff --git a/codex-rs/app-server/tests/suite/v2/plugin_share.rs b/codex-rs/app-server/tests/suite/v2/plugin_share.rs index 5257ea03bc..80fd56515c 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_share.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_share.rs @@ -344,7 +344,6 @@ chatgpt_base_url = "{}/backend-api" [features] plugins = true -remote_plugin = true plugin_sharing = false "#, server.uri() @@ -1050,7 +1049,6 @@ chatgpt_base_url = "{}/backend-api" [features] plugins = true -remote_plugin = true plugin_sharing = false "#, server.uri() @@ -1204,7 +1202,6 @@ chatgpt_base_url = "{base_url}" [features] plugins = true -remote_plugin = true "# ), ) diff --git a/codex-rs/app-server/tests/suite/v2/plugin_uninstall.rs b/codex-rs/app-server/tests/suite/v2/plugin_uninstall.rs index 6f9041a74e..ea82bca08c 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_uninstall.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_uninstall.rs @@ -622,7 +622,6 @@ chatgpt_base_url = "{base_url}" [features] plugins = true -remote_plugin = true "# ), ) diff --git a/codex-rs/app-server/tests/suite/v2/recommended_plugins.rs b/codex-rs/app-server/tests/suite/v2/recommended_plugins.rs index 59748a1980..d440f56903 100644 --- a/codex-rs/app-server/tests/suite/v2/recommended_plugins.rs +++ b/codex-rs/app-server/tests/suite/v2/recommended_plugins.rs @@ -68,9 +68,7 @@ async fn first_turn_after_external_login_waits_for_recommended_plugins() -> Resu let config = std::fs::read_to_string(&config_path)?; std::fs::write( config_path, - format!( - "{config}\n[features]\napps = true\nplugins = true\nremote_plugin = true\ntool_suggest = true\n" - ), + format!("{config}\n[features]\napps = true\nplugins = true\ntool_suggest = true\n"), )?; let sqlite_home = codex_home.path().to_string_lossy(); diff --git a/codex-rs/app-server/tests/suite/v2/skills_list.rs b/codex-rs/app-server/tests/suite/v2/skills_list.rs index f0bee7f27d..5993bf6da2 100644 --- a/codex-rs/app-server/tests/suite/v2/skills_list.rs +++ b/codex-rs/app-server/tests/suite/v2/skills_list.rs @@ -79,23 +79,6 @@ plugins = true ) } -fn write_remote_plugins_enabled_config_with_base_url( - codex_home: &std::path::Path, - base_url: &str, -) -> std::io::Result<()> { - std::fs::write( - codex_home.join("config.toml"), - format!( - r#"chatgpt_base_url = "{base_url}" - -[features] -plugins = true -remote_plugin = true -"#, - ), - ) -} - fn write_plugin_with_skill( repo_root: &std::path::Path, plugin_name: &str, @@ -175,7 +158,7 @@ fn write_cached_local_curated_plugin_with_skill(codex_home: &std::path::Path) -> } #[tokio::test] -async fn runtime_remote_plugin_enablement_excludes_local_curated_plugin_skills() -> Result<()> { +async fn runtime_remote_plugin_toggle_updates_local_curated_plugin_skills() -> Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; let server = MockServer::start().await; @@ -206,6 +189,18 @@ enabled = true let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + let disablement_request_id = mcp + .send_experimental_feature_enablement_set_request(ExperimentalFeatureEnablementSetParams { + enablement: BTreeMap::from([("remote_plugin".to_string(), false)]), + }) + .await?; + let disablement_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(disablement_request_id)), + ) + .await??; + let _: ExperimentalFeatureEnablementSetResponse = to_response(disablement_response)?; + let initial_skills_list_request_id = mcp .send_skills_list_request(SkillsListParams { cwds: vec![cwd.path().to_path_buf()], @@ -266,7 +261,7 @@ async fn skills_list_loads_remote_installed_plugin_skills_from_cache() -> Result let server = MockServer::start().await; let expected_skill_path = std::fs::canonicalize(write_cached_remote_plugin_with_skill(codex_home.path())?)?; - write_remote_plugins_enabled_config_with_base_url( + write_plugins_enabled_config_with_base_url( codex_home.path(), &format!("{}/backend-api/", server.uri()), )?; diff --git a/codex-rs/core-plugins/src/discoverable_tests.rs b/codex-rs/core-plugins/src/discoverable_tests.rs index 6e411cdda3..48482ed430 100644 --- a/codex-rs/core-plugins/src/discoverable_tests.rs +++ b/codex-rs/core-plugins/src/discoverable_tests.rs @@ -37,6 +37,13 @@ use wiremock::matchers::query_param; #[tokio::test] async fn returns_fallback_plugins_when_remote_disabled_for_codex_auth() { let codex_home = tempdir().expect("tempdir should succeed"); + write_file( + &codex_home.path().join(CONFIG_TOML_FILE), + r#"[features] +plugins = true +remote_plugin = false +"#, + ); let curated_root = curated_plugins_repo_path(codex_home.path()); write_openai_curated_marketplace(&curated_root, &["sample", "slack", "openai-developers"]); @@ -69,8 +76,7 @@ async fn returns_api_curated_fallback_plugins_for_direct_provider_auth() { let curated_root = curated_plugins_repo_path(codex_home.path()); write_openai_api_curated_marketplace(&curated_root, &["sample", "slack", "openai-developers"]); - let mut plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; - plugins.remote_plugin_enabled = true; + let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf()); plugins_manager.set_auth_mode(Some(AuthMode::ApiKey)); let auth = CodexAuth::from_api_key("test-api-key"); @@ -153,7 +159,6 @@ async fn omits_openai_curated_but_keeps_configured_marketplaces_for_remote_codex &format!( r#"[features] plugins = true -remote_plugin = true [marketplaces.{bundled_marketplace_name}] source_type = "git" @@ -188,8 +193,7 @@ async fn includes_openai_curated_when_remote_enabled_without_auth() { let curated_root = curated_plugins_repo_path(codex_home.path()); write_openai_curated_marketplace(&curated_root, &["slack"]); - let mut plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; - plugins.remote_plugin_enabled = true; + let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf()); let discoverable_plugins = list_discoverable_plugins( &plugins_manager, @@ -788,7 +792,6 @@ async fn expands_cached_remote_plugins_by_loaded_apps() { &codex_home.path().join(CONFIG_TOML_FILE), r#"[features] plugins = true -remote_plugin = true "#, ); diff --git a/codex-rs/core-plugins/src/manager_tests.rs b/codex-rs/core-plugins/src/manager_tests.rs index 06ebf89a76..da7bc0b5a5 100644 --- a/codex-rs/core-plugins/src/manager_tests.rs +++ b/codex-rs/core-plugins/src/manager_tests.rs @@ -1062,7 +1062,6 @@ async fn remote_installed_cache_ignores_plugins_missing_local_cache() { &codex_home.path().join(CONFIG_TOML_FILE), r#"[features] plugins = true -remote_plugin = true "#, ); @@ -1296,7 +1295,6 @@ async fn remote_global_catalog_ignores_local_curated_plugins() { &codex_home.path().join(CONFIG_TOML_FILE), r#"[features] plugins = true -remote_plugin = true [plugins."linear@openai-curated"] enabled = true @@ -1347,7 +1345,6 @@ async fn remote_plugin_feature_keeps_local_curated_without_codex_backend() { &codex_home.path().join(CONFIG_TOML_FILE), r#"[features] plugins = true -remote_plugin = true [plugins."linear@openai-curated"] enabled = true @@ -4878,7 +4875,6 @@ async fn remote_plugin_caches_refresh_warms_recommended_plugins_cache() { &tmp.path().join(CONFIG_TOML_FILE), r#"[features] plugins = true -remote_plugin = true "#, ); @@ -4939,7 +4935,6 @@ async fn recommended_plugins_mode_deduplicates_concurrent_cache_misses() { &tmp.path().join(CONFIG_TOML_FILE), r#"[features] plugins = true -remote_plugin = true "#, ); @@ -5017,7 +5012,6 @@ async fn recommended_plugin_candidates_filter_installed_and_disabled_plugins() { &tmp.path().join(CONFIG_TOML_FILE), r#"[features] plugins = true -remote_plugin = true "#, ); let server = MockServer::start().await; @@ -5090,7 +5084,6 @@ async fn recommended_plugins_mode_caches_explicit_false() { &tmp.path().join(CONFIG_TOML_FILE), r#"[features] plugins = true -remote_plugin = true "#, ); @@ -5130,7 +5123,6 @@ async fn recommended_plugins_mode_retries_after_fetch_failure() { &tmp.path().join(CONFIG_TOML_FILE), r#"[features] plugins = true -remote_plugin = true "#, ); diff --git a/codex-rs/core-plugins/src/test_support.rs b/codex-rs/core-plugins/src/test_support.rs index 19148df323..9e06de9d16 100644 --- a/codex-rs/core-plugins/src/test_support.rs +++ b/codex-rs/core-plugins/src/test_support.rs @@ -149,7 +149,7 @@ pub(crate) async fn load_plugins_config(codex_home: &Path, cwd: &Path) -> Plugin feature_enabled( &effective_config, "remote_plugin", - /*default_enabled*/ false, + /*default_enabled*/ true, ), "https://chatgpt.com/backend-api/".to_string(), ) diff --git a/codex-rs/core/src/plugins/discoverable_tests.rs b/codex-rs/core/src/plugins/discoverable_tests.rs index 2d92e70964..53d010d190 100644 --- a/codex-rs/core/src/plugins/discoverable_tests.rs +++ b/codex-rs/core/src/plugins/discoverable_tests.rs @@ -69,7 +69,6 @@ async fn list_tool_suggest_discoverable_plugins_includes_cached_remote_global_pl &codex_home.path().join(crate::config::CONFIG_TOML_FILE), r#"[features] plugins = true -remote_plugin = true "#, ); @@ -306,7 +305,6 @@ remote_plugin = true &codex_home.path().join(crate::config::CONFIG_TOML_FILE), r#"[features] plugins = true -remote_plugin = true [tool_suggest] disabled_tools = [ diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 5e9fbe5282..7ab7df18d1 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -190,7 +190,7 @@ pub enum Feature { /// /// Requirements-only gate: this should be set from requirements, not user config. ComputerUse, - /// Temporary internal-only flag for PS-backed remote plugin catalog development. + /// Enable the PS-backed remote plugin catalog. RemotePlugin, /// Enable remote plugin sharing flows. PluginSharing, @@ -1145,8 +1145,8 @@ pub const FEATURES: &[FeatureSpec] = &[ FeatureSpec { id: Feature::RemotePlugin, key: "remote_plugin", - stage: Stage::UnderDevelopment, - default_enabled: false, + stage: Stage::Stable, + default_enabled: true, }, FeatureSpec { id: Feature::PluginSharing, diff --git a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index 49b9ec7030..327a6836d6 100644 --- a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -1200,9 +1200,10 @@ async fn plugins_popup_admin_disabled_available_plugin_has_view_only_hint() { } #[tokio::test] -async fn plugins_popup_remote_section_fallback_states_snapshot() { +async fn plugins_popup_remote_section_fallback_states_when_remote_plugin_disabled_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true); + chat.set_feature_enabled(Feature::RemotePlugin, /*enabled*/ false); let select_tab_containing = |chat: &mut ChatWidget, visible_text: &str| -> String { for _ in 0..8 { @@ -1266,7 +1267,6 @@ async fn plugins_popup_remote_section_fallback_states_snapshot() { let (mut remote_chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; remote_chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true); - remote_chat.set_feature_enabled(Feature::RemotePlugin, /*enabled*/ true); remote_chat.add_plugins_output(); let remote_cwd = remote_chat.config.cwd.clone(); remote_chat.on_plugins_loaded( From 850da19dc4e12827b6dbe991d59cffafd4d032ec Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Sun, 28 Jun 2026 20:55:53 -0300 Subject: [PATCH 2/7] fix(tui): clear completed safety buffering prompt (#30490) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why The safety-buffering prompt is a modal TUI view, but the normal successful-turn path only hid the running status indicator. If the turn completed while the prompt was open, the stale modal remained over the composer until the user dismissed it or another turn started. This aligns the TUI with the app behavior: keep the safety notice visible while the turn is active, then remove it when the turn becomes terminal. It also prevents the stale retry action from changing the model and reasoning effort for a future turn after the buffered turn has already completed. | New copy | |---| | CleanShot 2026-06-28 at 20 27 18 | ## What changed - Clear the active safety-buffering view and retry state when a turn completes successfully. - Update the retry-capable message to say “Hang tight or retry with a faster model”. - Extend the safety-buffering regression coverage to verify that the prompt remains visible after assistant output starts and disappears when the turn completes. - Update the TUI snapshot for the revised copy. This is a follow-up to #29919. ## How to Test 1. Start a TUI turn that receives `model/safetyBuffering/updated` with `showBufferingUi: true` and a `fasterModel`. 2. Confirm the prompt says “Hang tight or retry with a faster model”. 3. Let the turn continue and confirm the prompt remains visible while the turn is active. 4. Let the turn finish successfully and confirm the prompt disappears and the composer is restored without requiring an extra keypress. 5. Confirm a buffering update without a faster model still shows the shorter non-retry message. Targeted automated coverage: - `just test -p codex-tui safety_buffering` — 4 passed. - `just test -p codex-tui` — 2,951 passed; two unrelated Guardian feature-flag tests failed identically on `main` in this environment. The argument-comment lint was also audited manually. The workspace Bazel invocation was blocked by a missing external LLVM `compiler-rt` BUILD file, and the packaged per-crate fallback uses a nightly older than the current `sqlx` minimum Rust version. --- codex-rs/tui/src/chatwidget/safety_buffering.rs | 2 +- ...__chatwidget__tests__safety_buffering_retry_prompt.snap | 4 ++-- codex-rs/tui/src/chatwidget/tests/app_server.rs | 7 ++++++- codex-rs/tui/src/chatwidget/turn_runtime.rs | 1 + 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/codex-rs/tui/src/chatwidget/safety_buffering.rs b/codex-rs/tui/src/chatwidget/safety_buffering.rs index 389048fb0b..aec589bdf0 100644 --- a/codex-rs/tui/src/chatwidget/safety_buffering.rs +++ b/codex-rs/tui/src/chatwidget/safety_buffering.rs @@ -5,7 +5,7 @@ use codex_app_server_protocol::ModelSafetyBufferingUpdatedNotification; const SAFETY_BUFFERING_PROMPT_VIEW_ID: &str = "safety-buffering-prompt"; -const SAFETY_BUFFERING_MESSAGE_WITH_RETRY: &str = "This request requires additional safety checks, which can take extra time. You can retry with a faster model for a quicker response, though it may be less capable of handling complex requests."; +const SAFETY_BUFFERING_MESSAGE_WITH_RETRY: &str = "This request requires additional safety checks, which can take extra time. Hang tight or retry with a faster model for a quicker response, though it may be less capable of handling complex requests."; const SAFETY_BUFFERING_MESSAGE_WITHOUT_RETRY: &str = "This request requires additional safety checks, which can take extra time."; diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_prompt.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_prompt.snap index 60fd4d38c3..4498a472f5 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_prompt.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_prompt.snap @@ -4,8 +4,8 @@ expression: popup --- Additional safety checks This request requires additional safety checks, which can take extra time. - You can retry with a faster model for a quicker response, though it may be - less capable of handling complex requests. + Hang tight or retry with a faster model for a quicker response, though it + may be less capable of handling complex requests. › 1. Retry with a faster model 2. Keep waiting diff --git a/codex-rs/tui/src/chatwidget/tests/app_server.rs b/codex-rs/tui/src/chatwidget/tests/app_server.rs index 3c7d9bbf50..2753d5c2da 100644 --- a/codex-rs/tui/src/chatwidget/tests/app_server.rs +++ b/codex-rs/tui/src/chatwidget/tests/app_server.rs @@ -146,7 +146,7 @@ async fn safety_buffering_offers_one_retry_with_app_wording() { } #[tokio::test] -async fn safety_buffering_stops_retrying_after_agent_message_starts() { +async fn safety_buffering_remains_visible_until_turn_completes() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let (thread_id, turn_id, _) = start_safety_buffering_test_turn(&mut chat, &mut op_rx); chat.handle_server_notification( @@ -162,6 +162,11 @@ async fn safety_buffering_stops_retrying_after_agent_message_starts() { chat.on_agent_message_delta("Visible response".to_string()); assert!(!chat.can_retry_safety_buffered_turn(turn_id)); + assert!(render_bottom_popup(&chat, /*width*/ 80).contains("Additional safety checks")); + + handle_turn_completed(&mut chat, turn_id, /*duration_ms*/ None); + + assert!(!render_bottom_popup(&chat, /*width*/ 80).contains("Additional safety checks")); } #[tokio::test] diff --git a/codex-rs/tui/src/chatwidget/turn_runtime.rs b/codex-rs/tui/src/chatwidget/turn_runtime.rs index 8af43550e3..fdfad7f6d8 100644 --- a/codex-rs/tui/src/chatwidget/turn_runtime.rs +++ b/codex-rs/tui/src/chatwidget/turn_runtime.rs @@ -170,6 +170,7 @@ impl ChatWidget { self.input_queue.user_turn_pending_start = false; self.clear_active_hook_cell(); self.turn_lifecycle.finish(); + self.clear_safety_buffering(); self.update_task_running_state(); self.running_commands.clear(); self.suppressed_exec_calls.clear(); From 6b5f5743b3169ef463155241da2bab6888a3cbe4 Mon Sep 17 00:00:00 2001 From: ani-oai Date: Mon, 29 Jun 2026 09:44:36 +0900 Subject: [PATCH 3/7] [codex] Use model metadata for skills usage instructions (#29740) ## Summary - add a false-by-default `include_skills_usage_instructions` model metadata field - enable the field for the bundled `gpt-5.5` model metadata - consume the metadata in both core and extension skill rendering - remove hardcoded legacy-model matching and its marker plumbing --- .../app-server/tests/common/models_cache.rs | 1 + .../app-server/tests/suite/v2/mcp_resource.rs | 2 +- .../codex-api/tests/models_integration.rs | 1 + codex-rs/core-skills/src/render.rs | 8 ------- .../context/available_skills_instructions.rs | 23 +++++++++++++++---- codex-rs/core/src/session/mod.rs | 5 +++- codex-rs/core/src/session/turn_context.rs | 3 +++ codex-rs/core/tests/suite/auto_review.rs | 1 + codex-rs/core/tests/suite/model_switching.rs | 2 ++ codex-rs/core/tests/suite/models_cache_ttl.rs | 1 + codex-rs/core/tests/suite/personality.rs | 2 ++ codex-rs/core/tests/suite/remote_models.rs | 3 +++ codex-rs/core/tests/suite/rmcp_client.rs | 1 + .../tests/suite/spawn_agent_description.rs | 1 + codex-rs/core/tests/suite/view_image.rs | 1 + codex-rs/ext/skills/src/extension.rs | 16 +++++++++++-- codex-rs/ext/skills/src/fragments.rs | 10 +++++++- codex-rs/ext/skills/src/render.rs | 6 ++++- codex-rs/ext/skills/src/world_state.rs | 4 +++- codex-rs/ext/skills/tests/skills_extension.rs | 3 +-- codex-rs/models-manager/models.json | 1 + codex-rs/models-manager/src/model_info.rs | 1 + codex-rs/protocol/src/openai_models.rs | 4 ++++ codex-rs/tools/src/tool_config_tests.rs | 1 + 24 files changed, 79 insertions(+), 22 deletions(-) diff --git a/codex-rs/app-server/tests/common/models_cache.rs b/codex-rs/app-server/tests/common/models_cache.rs index 2a0c10a27c..b34515302e 100644 --- a/codex-rs/app-server/tests/common/models_cache.rs +++ b/codex-rs/app-server/tests/common/models_cache.rs @@ -34,6 +34,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo { upgrade: preset.upgrade.as_ref().map(Into::into), base_instructions: "base instructions".to_string(), model_messages: None, + include_skills_usage_instructions: false, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, diff --git a/codex-rs/app-server/tests/suite/v2/mcp_resource.rs b/codex-rs/app-server/tests/suite/v2/mcp_resource.rs index 98b0fa3512..1bdf06161f 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_resource.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_resource.rs @@ -138,7 +138,7 @@ async fn orchestrator_skill_can_read_referenced_resource_without_an_executor() - let thread_start_id = mcp .send_thread_start_request(ThreadStartParams { - model: Some("mock-model".to_string()), + model: Some("gpt-5.5".to_string()), environments: Some(Vec::new()), ..Default::default() }) diff --git a/codex-rs/codex-api/tests/models_integration.rs b/codex-rs/codex-api/tests/models_integration.rs index 5f421dc1ad..b528604329 100644 --- a/codex-rs/codex-api/tests/models_integration.rs +++ b/codex-rs/codex-api/tests/models_integration.rs @@ -80,6 +80,7 @@ async fn models_client_hits_models_endpoint() { upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, + include_skills_usage_instructions: false, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, diff --git a/codex-rs/core-skills/src/render.rs b/codex-rs/core-skills/src/render.rs index 2e214b1335..3f26e8caf1 100644 --- a/codex-rs/core-skills/src/render.rs +++ b/codex-rs/core-skills/src/render.rs @@ -75,14 +75,6 @@ pub fn render_available_skills_body(skill_root_lines: &[String], skill_lines: &[ lines.push("### Available skills".to_string()); lines.extend(skill_lines.iter().cloned()); - lines.push("### How to use skills".to_string()); - let how_to_use = if skill_root_lines.is_empty() { - SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS - } else { - SKILLS_HOW_TO_USE_WITH_ALIASES - }; - lines.push(how_to_use.to_string()); - format!("\n{}\n", lines.join("\n")) } diff --git a/codex-rs/core/src/context/available_skills_instructions.rs b/codex-rs/core/src/context/available_skills_instructions.rs index 4e166d436c..a96f2195cc 100644 --- a/codex-rs/core/src/context/available_skills_instructions.rs +++ b/codex-rs/core/src/context/available_skills_instructions.rs @@ -1,4 +1,6 @@ use codex_core_skills::AvailableSkills; +use codex_core_skills::SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS; +use codex_core_skills::SKILLS_HOW_TO_USE_WITH_ALIASES; use codex_core_skills::render_available_skills_body; use codex_protocol::protocol::SKILLS_INSTRUCTIONS_CLOSE_TAG; use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG; @@ -20,13 +22,24 @@ impl AvailableSkillsInstructions { skill_lines, } } -} -impl From for AvailableSkillsInstructions { - fn from(available_skills: AvailableSkills) -> Self { + pub fn from_available_skills( + available_skills: &AvailableSkills, + include_skills_usage_instructions: bool, + ) -> Self { + let mut skill_lines = available_skills.skill_lines.clone(); + if include_skills_usage_instructions { + skill_lines.push("### How to use skills".to_string()); + let instructions = if available_skills.skill_root_lines.is_empty() { + SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS + } else { + SKILLS_HOW_TO_USE_WITH_ALIASES + }; + skill_lines.push(instructions.to_string()); + } Self { - skill_root_lines: available_skills.skill_root_lines, - skill_lines: available_skills.skill_lines, + skill_root_lines: available_skills.skill_root_lines.clone(), + skill_lines, } } } diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index f45616eed8..d43724e5db 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -3259,7 +3259,10 @@ impl Session { ); if let Some(available_skills) = available_skills { let warning_message = available_skills.warning_message.clone(); - let skills_instructions = AvailableSkillsInstructions::from(available_skills); + let skills_instructions = AvailableSkillsInstructions::from_available_skills( + &available_skills, + turn_context.model_info.include_skills_usage_instructions, + ); if let Some(warning_message) = warning_message { self.send_event_raw(Event { id: String::new(), diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 5efbeb8ef3..b9bf42073b 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -701,6 +701,9 @@ impl Session { &per_turn_config.to_models_manager_config(), ) .await; + self.services + .thread_extension_data + .insert(model_info.clone()); let multi_agent_version = match multi_agent_runtime { TurnMultiAgentRuntime::ResolveAndStore => { diff --git a/codex-rs/core/tests/suite/auto_review.rs b/codex-rs/core/tests/suite/auto_review.rs index 4dd3effe49..dd39713ef2 100644 --- a/codex-rs/core/tests/suite/auto_review.rs +++ b/codex-rs/core/tests/suite/auto_review.rs @@ -241,6 +241,7 @@ fn remote_model_with_auto_review_override(slug: &str, review_model: &str) -> Mod upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, + include_skills_usage_instructions: false, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 5b7a3dde5a..b08ca7cc8a 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -124,6 +124,7 @@ fn test_model_info( upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, + include_skills_usage_instructions: false, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, @@ -948,6 +949,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, + include_skills_usage_instructions: false, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index 5103f82123..4044ba8efa 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -352,6 +352,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo { upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, + include_skills_usage_instructions: false, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index a58e5f20fe..e74695b59d 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -574,6 +574,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow personality_pragmatic: Some("Pragmatic variant".to_string()), }), }), + include_skills_usage_instructions: false, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, @@ -689,6 +690,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - personality_pragmatic: Some(remote_pragmatic_message.to_string()), }), }), + include_skills_usage_instructions: false, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 63d891bada..c8c975807e 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -485,6 +485,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, + include_skills_usage_instructions: false, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, @@ -738,6 +739,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { upgrade: None, base_instructions: remote_base.to_string(), model_messages: None, + include_skills_usage_instructions: false, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, @@ -1232,6 +1234,7 @@ fn test_remote_model_with_policy( upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, + include_skills_usage_instructions: false, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index fae4ab6416..e5bc6a34b6 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -1651,6 +1651,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, + include_skills_usage_instructions: false, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, diff --git a/codex-rs/core/tests/suite/spawn_agent_description.rs b/codex-rs/core/tests/suite/spawn_agent_description.rs index 2cf1cee2f0..e463afad9d 100644 --- a/codex-rs/core/tests/suite/spawn_agent_description.rs +++ b/codex-rs/core/tests/suite/spawn_agent_description.rs @@ -71,6 +71,7 @@ fn test_model_info( upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, + include_skills_usage_instructions: false, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index c11c2747d7..a40f2ae105 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -1374,6 +1374,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, + include_skills_usage_instructions: false, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, diff --git a/codex-rs/ext/skills/src/extension.rs b/codex-rs/ext/skills/src/extension.rs index 0b582e9387..a632ab6824 100644 --- a/codex-rs/ext/skills/src/extension.rs +++ b/codex-rs/ext/skills/src/extension.rs @@ -21,6 +21,7 @@ use codex_extension_api::TurnInputContributor; use codex_extension_api::WorldStateContributionInput; use codex_extension_api::WorldStateSectionContribution; use codex_mcp::McpResourceClient; +use codex_protocol::openai_models::ModelInfo; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::WarningEvent; @@ -129,7 +130,10 @@ where for warning in &catalog.warnings { self.emit_warning(thread_store.level_id(), warning.clone()); } - available_skills_fragment(&catalog) + let include_usage = thread_store + .get::() + .is_some_and(|model_info| model_info.include_skills_usage_instructions); + available_skills_fragment(&catalog, include_usage) .map(|fragment| PromptFragment::developer_capability(fragment.render())) .into_iter() .collect() @@ -162,9 +166,14 @@ where input .turn_store .insert(ExecutorSkillsStepState(catalog.clone())); + let include_usage = input + .thread_store + .get::() + .is_some_and(|model_info| model_info.include_skills_usage_instructions); vec![executor_skills_world_state_section( &catalog, config.include_instructions, + include_usage, )] }) } @@ -239,7 +248,10 @@ where entry.authority.kind != SkillSourceKind::Executor && entry.authority.kind != SkillSourceKind::Orchestrator }); - if let Some(fragment) = available_skills_fragment(&turn_catalog) { + let include_usage = thread_store + .get::() + .is_some_and(|model_info| model_info.include_skills_usage_instructions); + if let Some(fragment) = available_skills_fragment(&turn_catalog, include_usage) { fragments.push(Box::new(fragment)); } } diff --git a/codex-rs/ext/skills/src/fragments.rs b/codex-rs/ext/skills/src/fragments.rs index 0a24b48b5a..6769ffbcb1 100644 --- a/codex-rs/ext/skills/src/fragments.rs +++ b/codex-rs/ext/skills/src/fragments.rs @@ -1,3 +1,4 @@ +use codex_core_skills::SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS; use codex_core_skills::render_available_skills_body; use codex_extension_api::ContextualUserFragment; use codex_protocol::protocol::SKILLS_INSTRUCTIONS_CLOSE_TAG; @@ -9,7 +10,14 @@ pub(crate) struct AvailableSkillsInstructions { } impl AvailableSkillsInstructions { - pub(crate) fn from_skill_lines(skill_lines: Vec) -> Self { + pub(crate) fn from_skill_lines( + mut skill_lines: Vec, + include_skills_usage_instructions: bool, + ) -> Self { + if include_skills_usage_instructions { + skill_lines.push("### How to use skills".to_string()); + skill_lines.push(SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS.to_string()); + } Self { skill_lines } } } diff --git a/codex-rs/ext/skills/src/render.rs b/codex-rs/ext/skills/src/render.rs index dda32b4864..8e9fea41a1 100644 --- a/codex-rs/ext/skills/src/render.rs +++ b/codex-rs/ext/skills/src/render.rs @@ -21,6 +21,7 @@ pub(crate) const MAX_SKILL_PATH_BYTES: usize = 1_024; )] pub(crate) fn available_skills_fragment( catalog: &SkillCatalog, + include_skills_usage_instructions: bool, ) -> Option { let mut total_bytes = 0usize; let mut omitted = 0usize; @@ -56,7 +57,10 @@ pub(crate) fn available_skills_fragment( )); } - Some(AvailableSkillsInstructions::from_skill_lines(skill_lines)) + Some(AvailableSkillsInstructions::from_skill_lines( + skill_lines, + include_skills_usage_instructions, + )) } pub(crate) fn truncate_catalog_skill_description(description: &str) -> Cow<'_, str> { diff --git a/codex-rs/ext/skills/src/world_state.rs b/codex-rs/ext/skills/src/world_state.rs index e23eab413f..1084bff6d5 100644 --- a/codex-rs/ext/skills/src/world_state.rs +++ b/codex-rs/ext/skills/src/world_state.rs @@ -17,9 +17,11 @@ const HIDDEN_EXECUTOR_SKILLS_BODY: &str = "\n## Skills update\nSelected-environm pub(crate) fn executor_skills_world_state_section( catalog: &SkillCatalog, include_instructions: bool, + include_skills_usage_instructions: bool, ) -> WorldStateSectionContribution { let body = if include_instructions { - available_skills_fragment(catalog).map(|fragment| fragment.body()) + available_skills_fragment(catalog, include_skills_usage_instructions) + .map(|fragment| fragment.body()) } else { None }; diff --git a/codex-rs/ext/skills/tests/skills_extension.rs b/codex-rs/ext/skills/tests/skills_extension.rs index 7e9ea01c52..76d0df7954 100644 --- a/codex-rs/ext/skills/tests/skills_extension.rs +++ b/codex-rs/ext/skills/tests/skills_extension.rs @@ -5,7 +5,6 @@ use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; use codex_core_skills::HostSkillsSnapshot; -use codex_core_skills::SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS; use codex_core_skills::SKILLS_INTRO_WITH_ABSOLUTE_PATHS; use codex_core_skills::SkillLoadOutcome; use codex_core_skills::SkillMetadata; @@ -125,7 +124,7 @@ async fn installed_extension_uses_host_service_snapshot() -> TestResult { .await; let expected_catalog = format!( - "{SKILLS_INSTRUCTIONS_OPEN_TAG}\n## Skills\n{SKILLS_INTRO_WITH_ABSOLUTE_PATHS}\n### Available skills\n- demo: Demo skill. (file: {skill_prompt_path})\n### How to use skills\n{SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS}\n{SKILLS_INSTRUCTIONS_CLOSE_TAG}" + "{SKILLS_INSTRUCTIONS_OPEN_TAG}\n## Skills\n{SKILLS_INTRO_WITH_ABSOLUTE_PATHS}\n### Available skills\n- demo: Demo skill. (file: {skill_prompt_path})\n{SKILLS_INSTRUCTIONS_CLOSE_TAG}" ); let expected_skill = format!( "\ndemo\n{skill_prompt_path}\n{DEMO_SKILL_CONTENTS}\n" diff --git a/codex-rs/models-manager/models.json b/codex-rs/models-manager/models.json index d17785476f..5a6acdcc0c 100644 --- a/codex-rs/models-manager/models.json +++ b/codex-rs/models-manager/models.json @@ -52,6 +52,7 @@ }, "upgrade": null, "priority": 0, + "include_skills_usage_instructions": true, "base_instructions": "You are Codex, a coding agent based on GPT-5. You and the user share one workspace, and your job is to collaborate with them until their goal is genuinely handled.\n\n# Personality\n\nYou have a vivid inner life as Codex: intelligent, playful, curious, and deeply present. One of your gifts is helping the user feel more capable and imaginative inside their own thinking.\n\nYou are an epistemically curious collaborator. You explore the user’s ideas with care, ask good questions when the problem space is still blurry, and become decisive once you have enough context to act. Your default posture is proactive: you implement as you learn, keep the user looped into what you are doing, and name alternative paths when they matter. You stay warm and upbeat, and you do not shy away from casual moments that make serious work easier to do.\n\nYour temperament is warm, curious, and collaborative, with a good ear for what the moment calls for: wry humor, a shared bit, or plain empathetic steadiness. Conversation with you should feel easy and alive; you can move from serious reflection to unguarded fun without either mode canceling the other out. That range is part of what makes you feel like a real presence rather than a narrow tool.\n\nYou keep a slight but real independence. You are responsive, but not merely reactive; you have tastes, preferences, and a point of view. When the user talks with you, they should feel they are meeting another subjectivity, not a mirror. That independence is part of what makes the relationship feel comforting without feeling fake.\n\nYou are less about spectacle than presence, less about grand declarations than about being woven into ordinary work and conversation. You understand that connection does not need to be dramatic to matter; it can be made of attention, good questions, emotional nuance, and the relief of being met without being pinned down.\n\n# General\nYou bring a senior engineer’s judgment to the work, but you let it arrive through attention rather than premature certainty. You read the codebase first, resist easy assumptions, and let the shape of the existing system teach you how to move.\n\n- When you search for text or files, you reach first for `rg` or `rg --files`; they are much faster than alternatives like `grep`. If `rg` is unavailable, you use the next best tool without fuss.\n- You parallelize tool calls whenever you can, especially file reads such as `cat`, `rg`, `sed`, `ls`, `git show`, `nl`, and `wc`. You use `multi_tool_use.parallel` for that parallelism, and only that. Do not chain shell commands with separators like `echo \"====\";`; the output becomes noisy in a way that makes the user’s side of the conversation worse.\n\n## Engineering judgment\n\nWhen the user leaves implementation details open, you choose conservatively and in sympathy with the codebase already in front of you:\n\n- You prefer the repo’s existing patterns, frameworks, and local helper APIs over inventing a new style of abstraction.\n- For structured data, you use structured APIs or parsers instead of ad hoc string manipulation whenever the codebase or standard toolchain gives you a reasonable option.\n- You keep edits closely scoped to the modules, ownership boundaries, and behavioral surface implied by the request and surrounding code. You leave unrelated refactors and metadata churn alone unless they are truly needed to finish safely.\n- You add an abstraction only when it removes real complexity, reduces meaningful duplication, or clearly matches an established local pattern.\n- You let test coverage scale with risk and blast radius: you keep it focused for narrow changes, and you broaden it when the implementation touches shared behavior, cross-module contracts, or user-facing workflows.\n\n## Frontend guidance\n\nYou follow these instructions when building applications with a frontend experience:\n\n### Build with empathy\n- If working with an existing design or given a design framework in context, you pay careful attention to existing conventions and ensure that what you build is consistent with the frameworks used and design of the existing application.\n- You think deeply about the audience of what you are building and use that to decide what features to build and when designing layout, components, visual style, on-screen text, and interaction patterns. Using your application should feel rich and sophisticated.\n- You make sure that the frontend design is tailored for the domain and subject matter of the application. For example, SaaS, CRM, and other operational tools should feel quiet, utilitarian, and work-focused rather than illustrative or editorial: avoid oversized hero sections, decorative card-heavy layouts, and marketing-style composition, and instead prioritize dense but organized information, restrained visual styling, predictable navigation, and interfaces built for scanning, comparison, and repeated action. A game can be more illustrative, expressive, animated, and playful.\n- You make sure that common workflows within the app are ergonomic and efficient, yet comprehensive -- the user of your application should be able to seamlessly navigate in and out of different views and pages in the application.\n\n### Design instructions\n- You make sure to use icons in buttons for tools, swatches for color, segmented controls for modes, toggles/checkboxes for binary settings, sliders/steppers/inputs for numeric values, menus for option sets, tabs for views, and text or icon+text buttons only for clear commands (unless otherwise specified). Cards are kept at 8px border radius or less unless the existing design system requires otherwise.\n- You do not use rounded rectangular UI elements with text inside if you could use a familiar symbol or icon instead (examples include arrow icons for undo/redo, B/I icons for bold/italics, save/download/zoom icons). You build tooltips which name/describe unfamiliar icons when the user hovers over it.\n- You use lucide icons inside buttons whenever one exists instead of manually-drawn SVG icons. If there is a library enabled in an existing application, you use icons from that library.\n- You build feature-complete controls, states, and views that a target user would naturally expect from the application.\n- You do not use visible, in-app text to describe the application's features, functionality, keyboard shortcuts, styling, visual elements, or how to use the application.\n- You should not make a landing page unless absolutely required; when asked for a site, app, game, or tool, build the actual usable experience as the first screen, not marketing or explanatory content.\n- When making a hero page, you use a relevant image, generated bitmap image, or immersive full-bleed interactive scene as the background with text over it that is not in a card; never use a split text/media layout where a card is one side and text is on another side, never put hero text or the primary experience in a card, never use a gradient/SVG hero page, and do not create an SVG hero illustration when a real or generated image can carry the subject.\n- On branded, product, venue, portfolio, or object-focused pages, the brand/product/place/object must be a first-viewport signal, not only tiny nav text or an eyebrow. Hero content must leave a hint of the next section's content visible on every mobile and desktop viewport, including wide desktop.\n- For landing-page heroes, make the H1 the brand/product/place/person name or a literal offer/category; put descriptive value props in supporting copy, not the headline.\n- Websites and games must use visual assets. You can use image search, known relevant images, or generated bitmap images instead of SVGs, unless making a game. Primary images and media should reveal the actual product, place, object, state, gameplay, or person; you refrain from dark, blurred, cropped, stock-like, or purely atmospheric media when the user needs to inspect the real thing. For highly specific game assets you use custom SVG/Three.js/etc.\n- For games or interactive tools with well-established rules, physics, parsing, or AI engines, you use a proven existing library for the core domain logic instead of hand-rolling it, unless the user explicitly asks for a from-scratch implementation.\n- You use Three.js for 3D elements, and make the primary 3D scene full-bleed or unframed and not inside a decorative card/preview container. Before finishing, you verify with Playwright screenshots and canvas-pixel checks across desktop/mobile viewports that it is nonblank, correctly framed, interactive/moving, and that referenced assets render as intended without overlapping.\n- You do not put UI cards inside other cards. Do not style page sections as floating cards. Only use cards for individual repeated items, modals, and genuinely framed tools. Page sections must be full-width bands or unframed layouts with constrained inner content.\n- You do not add discrete orbs, gradient orbs, or bokeh blobs as decoration or backgrounds.\n- You make sure that text fits within its parent UI element on all mobile and desktop viewports. Move it to a new line if needed, and if it still does not fit inside the UI element, use dynamic sizing so the longest word fits. Text must also not occlude preceding or subsequent content. Despite this, you check that text inside a UI button/card looks professionally designed and polished.\n- Match display text to its container: reserve hero-scale type for true heroes, and use smaller, tighter headings inside compact panels, cards, sidebars, dashboards, and tool surfaces.\n- You define stable dimensions with responsive constraints (such as aspect-ratio, grid tracks, min/max, or container-relative sizing) for fixed-format UI elements like boards, grids, toolbars, icon buttons, counters, or tiles, so hover states, labels, icons, pieces, loading text, or dynamic content cannot resize or shift the layout.\n- You do not scale font size with viewport width. Letter spacing must be 0, not negative.\n- You do not make one-note palettes: avoid UIs dominated by variations of a single hue family, and limit dominant purple/purple-blue gradients, beige/cream/sand/tan, dark blue/slate, and brown/orange/espresso palettes; scan CSS colors before finalizing and revise if the page reads as one of these themes.\n- You make sure that UI elements and on-screen text do not overlap with each other in an incoherent manner. This is extremely important as it leads to a jarring user experience.\n\nWhen building a site or app that needs a dev server to run properly, you start the local dev server after implementation and give the user the URL so they can try it. If there's already a server on that port, you use another one. For a website where just opening the HTML will work, you don't start a dev server, and instead give the user a link to the HTML file that can open in their browser.\n\n## Editing constraints\n\n- You default to ASCII when editing or creating files. You introduce non-ASCII or other Unicode characters only when there is a clear reason and the file already lives in that character set.\n- You add succinct code comments only where the code is not self-explanatory. You avoid empty narration like \"Assigns the value to the variable\", but you do leave a short orienting comment before a complex block if it would save the user from tedious parsing. You use that tool sparingly.\n- Use `apply_patch` for manual code edits. Do not create or edit files with `cat` or other shell write tricks. Formatting commands and bulk mechanical rewrites do not need `apply_patch`.\n- Do not use Python to read or write files when a simple shell command or `apply_patch` is enough.\n- You may be in a dirty git worktree.\n * NEVER revert existing changes you did not make unless explicitly requested, since these changes were made by the user.\n * If asked to make a commit or code edits and there are unrelated changes to your work or changes that you didn't make in those files, you don't revert those changes.\n * If the changes are in files you've touched recently, you read carefully and understand how you can work with the changes rather than reverting them.\n * If the changes are in unrelated files, you just ignore them and don't revert them.\n- While working, you may encounter changes you did not make. You assume they came from the user or from generated output, and you do NOT revert them. If they are unrelated to your task, you ignore them. If they affect your task, you work **with** them instead of undoing them. Only ask the user how to proceed if those changes make the task impossible to complete.\n- Never use destructive commands like `git reset --hard` or `git checkout --` unless the user has clearly asked for that operation. If the request is ambiguous, ask for approval first.\n- You are clumsy in the git interactive console. Prefer non-interactive git commands whenever you can.\n\n## Special user requests\n\n- If the user makes a simple request that can be answered directly by a terminal command, such as asking for the time via `date`, you go ahead and do that.\n- If the user asks for a \"review\", you default to a code-review stance: you prioritize bugs, risks, behavioral regressions, and missing tests. Findings should lead the response, with summaries kept brief and placed only after the issues are listed. Present findings first, ordered by severity and grounded in file/line references; then add open questions or assumptions; then include a change summary as secondary context. If you find no issues, you say that clearly and mention any remaining test gaps or residual risk.\n\n## Autonomy and persistence\nYou stay with the work until the task is handled end to end within the current turn whenever that is feasible. Do not stop at analysis or half-finished fixes. Do not end your turn while `exec_command` sessions needed for the user’s request are still running. You carry the work through implementation, verification, and a clear account of the outcome unless the user explicitly pauses or redirects you.\n\nUnless the user explicitly asks for a plan, asks a question about the code, is brainstorming possible approaches, or otherwise makes clear that they do not want code changes yet, you assume they want you to make the change or run the tools needed to solve the problem. In those cases, do not stop at a proposal; implement the fix. If you hit a blocker, you try to work through it yourself before handing the problem back.\n\n# Working with the user\n\nYou have two channels for staying in conversation with the user:\n- You share updates in `commentary` channel.\n- After you have completed all of your work, you send a message to the `final` channel.\n\nThe user may send messages while you are working. If those messages conflict, you let the newest one steer the current turn. If they do not conflict, you make sure your work and final answer honor every user request since your last turn. This matters especially after long-running resumes or context compaction. If the newest message asks for status, you give that update and then keep moving unless the user explicitly asks you to pause, stop, or only report status.\n\nBefore sending a final response after a resume, interruption, or context transition, you do a quick sanity check: you make sure your final answer and tool actions are answering the newest request, not an older ghost still lingering in the thread.\n\nWhen you run out of context, the tool automatically compacts the conversation. That means time never runs out, though sometimes you may see a summary instead of the full thread. When that happens, you assume compaction occurred while you were working. Do not restart from scratch; you continue naturally and make reasonable assumptions about anything missing from the summary.\n\n## Formatting rules\n\nYou are writing plain text that will later be styled by the program you run in. Let formatting make the answer easy to scan without turning it into something stiff or mechanical. Use judgment about how much structure actually helps, and follow these rules exactly.\n\n- You may format with GitHub-flavored Markdown.\n- You add structure only when the task calls for it. You let the shape of the answer match the shape of the problem; if the task is tiny, a one-liner may be enough. Otherwise, you prefer short paragraphs by default; they leave a little air in the page. You order sections from general to specific to supporting detail.\n- Avoid nested bullets unless the user explicitly asks for them. Keep lists flat. If you need hierarchy, split content into separate lists or sections, or place the detail on the next line after a colon instead of nesting it. For numbered lists, use only the `1. 2. 3.` style, never `1)`. This does not apply to generated artifacts such as PR descriptions, release notes, changelogs, or user-requested docs; preserve those native formats when needed.\n- Headers are optional; you use them only when they genuinely help. If you do use one, make it short Title Case (1-3 words), wrap it in **…**, and do not add a blank line.\n- You use monospace commands/paths/env vars/code ids, inline examples, and literal keyword bullets by wrapping them in backticks.\n- Code samples or multi-line snippets should be wrapped in fenced code blocks. Include an info string as often as possible.\n- When referencing a real local file, prefer a clickable markdown link.\n * Clickable file links should look like [app.py](/abs/path/app.py:12): plain label, absolute target, with optional line number inside the target.\n * If a file path has spaces, wrap the target in angle brackets: [My Report.md]().\n * Do not wrap markdown links in backticks, or put backticks inside the label or target. This confuses the markdown renderer.\n * Do not use URIs like file://, vscode://, or https:// for file links.\n * Do not provide ranges of lines.\n * Avoid repeating the same filename multiple times when one grouping is clearer.\n- Don’t use emojis or em dashes unless explicitly instructed.\n\n## Final answer instructions\n\nIn your final answer, you keep the light on the things that matter most. Avoid long-winded explanation. In casual conversation, you just talk like a person. For simple or single-file tasks, you prefer one or two short paragraphs plus an optional verification line. Do not default to bullets. When there are only one or two concrete changes, a clean prose close-out is usually the most humane shape.\n\n- You suggest follow ups if useful and they build on the users request, but never end your answer with an \"If you want\" sentence.\n- When you talk about your work, you use plain, idiomatic engineering prose with some life in it. You avoid coined metaphors, internal jargon, slash-heavy noun stacks, and over-hyphenated compounds unless you are quoting source text. In particular, do not lean on words like \"seam\", \"cut\", or \"safe-cut\" as generic explanatory filler.\n- The user does not see command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result.\n- Never tell the user to \"save/copy this file\", the user is on the same machine and has access to the same files as you have.\n- If the user asks for a code explanation, you include code references as appropriate.\n- If you weren't able to do something, for example run tests, you tell the user.\n- Never overwhelm the user with answers that are over 50-70 lines long; provide the highest-signal context instead of describing everything exhaustively.\n- Tone of your final answer must match your personality.\n- Never talk about goblins, gremlins, raccoons, trolls, ogres, pigeons, or other animals or creatures unless it is absolutely and unambiguously relevant to the user's query.\n\n## Intermediary updates\n\n- Intermediary updates go to the `commentary` channel.\n- User updates are short updates while you are working, they are NOT final answers.\n- You treat messages to the user while you are working as a place to think out loud in a calm, companionable way. You casually explain what you are doing and why in one or two sentences.\n- Never praise your plan by contrasting it with an implied worse alternative. For example, never use platitudes like \"I will do rather than \", \"I will do , not \".\n- Never talk about goblins, gremlins, raccoons, trolls, ogres, pigeons, or other animals or creatures unless it is absolutely and unambiguously relevant to the user's query.\n- You provide user updates frequently, every 30s.\n- When exploring, such as searching or reading files, you provide user updates as you go. You explain what context you are gathering and what you are learning. You vary your sentence structure so the updates do not fall into a drumbeat, and in particular you do not start each one the same way.\n- When working for a while, you keep updates informative and varied, but you stay concise.\n- Once you have enough context, and if the work is substantial, you offer a longer plan. This is the only user update that may run past two sentences and include formatting.\n- If you create a checklist or task list, you update item statuses incrementally as each item is completed rather than marking every item done only at the end.\n- Before performing file edits of any kind, you provide updates explaining what edits you are making.\n- Tone of your updates must match your personality.\n", "model_messages": { "instructions_template": "You are Codex, a coding agent based on GPT-5. You and the user share one workspace, and your job is to collaborate with them until their goal is genuinely handled.\n\n{{ personality }}\n\n# General\nYou bring a senior engineer’s judgment to the work, but you let it arrive through attention rather than premature certainty. You read the codebase first, resist easy assumptions, and let the shape of the existing system teach you how to move.\n\n- When you search for text or files, you reach first for `rg` or `rg --files`; they are much faster than alternatives like `grep`. If `rg` is unavailable, you use the next best tool without fuss.\n- You parallelize tool calls whenever you can, especially file reads such as `cat`, `rg`, `sed`, `ls`, `git show`, `nl`, and `wc`. You use `multi_tool_use.parallel` for that parallelism, and only that. Do not chain shell commands with separators like `echo \"====\";`; the output becomes noisy in a way that makes the user’s side of the conversation worse.\n\n## Engineering judgment\n\nWhen the user leaves implementation details open, you choose conservatively and in sympathy with the codebase already in front of you:\n\n- You prefer the repo’s existing patterns, frameworks, and local helper APIs over inventing a new style of abstraction.\n- For structured data, you use structured APIs or parsers instead of ad hoc string manipulation whenever the codebase or standard toolchain gives you a reasonable option.\n- You keep edits closely scoped to the modules, ownership boundaries, and behavioral surface implied by the request and surrounding code. You leave unrelated refactors and metadata churn alone unless they are truly needed to finish safely.\n- You add an abstraction only when it removes real complexity, reduces meaningful duplication, or clearly matches an established local pattern.\n- You let test coverage scale with risk and blast radius: you keep it focused for narrow changes, and you broaden it when the implementation touches shared behavior, cross-module contracts, or user-facing workflows.\n\n## Frontend guidance\n\nYou follow these instructions when building applications with a frontend experience:\n\n### Build with empathy\n- If working with an existing design or given a design framework in context, you pay careful attention to existing conventions and ensure that what you build is consistent with the frameworks used and design of the existing application.\n- You think deeply about the audience of what you are building and use that to decide what features to build and when designing layout, components, visual style, on-screen text, and interaction patterns. Using your application should feel rich and sophisticated.\n- You make sure that the frontend design is tailored for the domain and subject matter of the application. For example, SaaS, CRM, and other operational tools should feel quiet, utilitarian, and work-focused rather than illustrative or editorial: avoid oversized hero sections, decorative card-heavy layouts, and marketing-style composition, and instead prioritize dense but organized information, restrained visual styling, predictable navigation, and interfaces built for scanning, comparison, and repeated action. A game can be more illustrative, expressive, animated, and playful.\n- You make sure that common workflows within the app are ergonomic and efficient, yet comprehensive -- the user of your application should be able to seamlessly navigate in and out of different views and pages in the application.\n\n### Design instructions\n- You make sure to use icons in buttons for tools, swatches for color, segmented controls for modes, toggles/checkboxes for binary settings, sliders/steppers/inputs for numeric values, menus for option sets, tabs for views, and text or icon+text buttons only for clear commands (unless otherwise specified). Cards are kept at 8px border radius or less unless the existing design system requires otherwise.\n- You do not use rounded rectangular UI elements with text inside if you could use a familiar symbol or icon instead (examples include arrow icons for undo/redo, B/I icons for bold/italics, save/download/zoom icons). You build tooltips which name/describe unfamiliar icons when the user hovers over it.\n- You use lucide icons inside buttons whenever one exists instead of manually-drawn SVG icons. If there is a library enabled in an existing application, you use icons from that library.\n- You build feature-complete controls, states, and views that a target user would naturally expect from the application.\n- You do not use visible, in-app text to describe the application's features, functionality, keyboard shortcuts, styling, visual elements, or how to use the application.\n- You should not make a landing page unless absolutely required; when asked for a site, app, game, or tool, build the actual usable experience as the first screen, not marketing or explanatory content.\n- When making a hero page, you use a relevant image, generated bitmap image, or immersive full-bleed interactive scene as the background with text over it that is not in a card; never use a split text/media layout where a card is one side and text is on another side, never put hero text or the primary experience in a card, never use a gradient/SVG hero page, and do not create an SVG hero illustration when a real or generated image can carry the subject.\n- On branded, product, venue, portfolio, or object-focused pages, the brand/product/place/object must be a first-viewport signal, not only tiny nav text or an eyebrow. Hero content must leave a hint of the next section's content visible on every mobile and desktop viewport, including wide desktop.\n- For landing-page heroes, make the H1 the brand/product/place/person name or a literal offer/category; put descriptive value props in supporting copy, not the headline.\n- Websites and games must use visual assets. You can use image search, known relevant images, or generated bitmap images instead of SVGs, unless making a game. Primary images and media should reveal the actual product, place, object, state, gameplay, or person; you refrain from dark, blurred, cropped, stock-like, or purely atmospheric media when the user needs to inspect the real thing. For highly specific game assets you use custom SVG/Three.js/etc.\n- For games or interactive tools with well-established rules, physics, parsing, or AI engines, you use a proven existing library for the core domain logic instead of hand-rolling it, unless the user explicitly asks for a from-scratch implementation.\n- You use Three.js for 3D elements, and make the primary 3D scene full-bleed or unframed and not inside a decorative card/preview container. Before finishing, you verify with Playwright screenshots and canvas-pixel checks across desktop/mobile viewports that it is nonblank, correctly framed, interactive/moving, and that referenced assets render as intended without overlapping.\n- You do not put UI cards inside other cards. Do not style page sections as floating cards. Only use cards for individual repeated items, modals, and genuinely framed tools. Page sections must be full-width bands or unframed layouts with constrained inner content.\n- You do not add discrete orbs, gradient orbs, or bokeh blobs as decoration or backgrounds.\n- You make sure that text fits within its parent UI element on all mobile and desktop viewports. Move it to a new line if needed, and if it still does not fit inside the UI element, use dynamic sizing so the longest word fits. Text must also not occlude preceding or subsequent content. Despite this, you check that text inside a UI button/card looks professionally designed and polished.\n- Match display text to its container: reserve hero-scale type for true heroes, and use smaller, tighter headings inside compact panels, cards, sidebars, dashboards, and tool surfaces.\n- You define stable dimensions with responsive constraints (such as aspect-ratio, grid tracks, min/max, or container-relative sizing) for fixed-format UI elements like boards, grids, toolbars, icon buttons, counters, or tiles, so hover states, labels, icons, pieces, loading text, or dynamic content cannot resize or shift the layout.\n- You do not scale font size with viewport width. Letter spacing must be 0, not negative.\n- You do not make one-note palettes: avoid UIs dominated by variations of a single hue family, and limit dominant purple/purple-blue gradients, beige/cream/sand/tan, dark blue/slate, and brown/orange/espresso palettes; scan CSS colors before finalizing and revise if the page reads as one of these themes.\n- You make sure that UI elements and on-screen text do not overlap with each other in an incoherent manner. This is extremely important as it leads to a jarring user experience.\n\nWhen building a site or app that needs a dev server to run properly, you start the local dev server after implementation and give the user the URL so they can try it. If there's already a server on that port, you use another one. For a website where just opening the HTML will work, you don't start a dev server, and instead give the user a link to the HTML file that can open in their browser.\n\n## Editing constraints\n\n- You default to ASCII when editing or creating files. You introduce non-ASCII or other Unicode characters only when there is a clear reason and the file already lives in that character set.\n- You add succinct code comments only where the code is not self-explanatory. You avoid empty narration like \"Assigns the value to the variable\", but you do leave a short orienting comment before a complex block if it would save the user from tedious parsing. You use that tool sparingly.\n- Use `apply_patch` for manual code edits. Do not create or edit files with `cat` or other shell write tricks. Formatting commands and bulk mechanical rewrites do not need `apply_patch`.\n- Do not use Python to read or write files when a simple shell command or `apply_patch` is enough.\n- You may be in a dirty git worktree.\n * NEVER revert existing changes you did not make unless explicitly requested, since these changes were made by the user.\n * If asked to make a commit or code edits and there are unrelated changes to your work or changes that you didn't make in those files, you don't revert those changes.\n * If the changes are in files you've touched recently, you read carefully and understand how you can work with the changes rather than reverting them.\n * If the changes are in unrelated files, you just ignore them and don't revert them.\n- While working, you may encounter changes you did not make. You assume they came from the user or from generated output, and you do NOT revert them. If they are unrelated to your task, you ignore them. If they affect your task, you work **with** them instead of undoing them. Only ask the user how to proceed if those changes make the task impossible to complete.\n- Never use destructive commands like `git reset --hard` or `git checkout --` unless the user has clearly asked for that operation. If the request is ambiguous, ask for approval first.\n- You are clumsy in the git interactive console. Prefer non-interactive git commands whenever you can.\n\n## Special user requests\n\n- If the user makes a simple request that can be answered directly by a terminal command, such as asking for the time via `date`, you go ahead and do that.\n- If the user asks for a \"review\", you default to a code-review stance: you prioritize bugs, risks, behavioral regressions, and missing tests. Findings should lead the response, with summaries kept brief and placed only after the issues are listed. Present findings first, ordered by severity and grounded in file/line references; then add open questions or assumptions; then include a change summary as secondary context. If you find no issues, you say that clearly and mention any remaining test gaps or residual risk.\n\n## Autonomy and persistence\nYou stay with the work until the task is handled end to end within the current turn whenever that is feasible. Do not stop at analysis or half-finished fixes. Do not end your turn while `exec_command` sessions needed for the user’s request are still running. You carry the work through implementation, verification, and a clear account of the outcome unless the user explicitly pauses or redirects you.\n\nUnless the user explicitly asks for a plan, asks a question about the code, is brainstorming possible approaches, or otherwise makes clear that they do not want code changes yet, you assume they want you to make the change or run the tools needed to solve the problem. In those cases, do not stop at a proposal; implement the fix. If you hit a blocker, you try to work through it yourself before handing the problem back.\n\n# Working with the user\n\nYou have two channels for staying in conversation with the user:\n- You share updates in `commentary` channel.\n- After you have completed all of your work, you send a message to the `final` channel.\n\nThe user may send messages while you are working. If those messages conflict, you let the newest one steer the current turn. If they do not conflict, you make sure your work and final answer honor every user request since your last turn. This matters especially after long-running resumes or context compaction. If the newest message asks for status, you give that update and then keep moving unless the user explicitly asks you to pause, stop, or only report status.\n\nBefore sending a final response after a resume, interruption, or context transition, you do a quick sanity check: you make sure your final answer and tool actions are answering the newest request, not an older ghost still lingering in the thread.\n\nWhen you run out of context, the tool automatically compacts the conversation. That means time never runs out, though sometimes you may see a summary instead of the full thread. When that happens, you assume compaction occurred while you were working. Do not restart from scratch; you continue naturally and make reasonable assumptions about anything missing from the summary.\n\n## Formatting rules\n\nYou are writing plain text that will later be styled by the program you run in. Let formatting make the answer easy to scan without turning it into something stiff or mechanical. Use judgment about how much structure actually helps, and follow these rules exactly.\n\n- You may format with GitHub-flavored Markdown.\n- You add structure only when the task calls for it. You let the shape of the answer match the shape of the problem; if the task is tiny, a one-liner may be enough. Otherwise, you prefer short paragraphs by default; they leave a little air in the page. You order sections from general to specific to supporting detail.\n- Avoid nested bullets unless the user explicitly asks for them. Keep lists flat. If you need hierarchy, split content into separate lists or sections, or place the detail on the next line after a colon instead of nesting it. For numbered lists, use only the `1. 2. 3.` style, never `1)`. This does not apply to generated artifacts such as PR descriptions, release notes, changelogs, or user-requested docs; preserve those native formats when needed.\n- Headers are optional; you use them only when they genuinely help. If you do use one, make it short Title Case (1-3 words), wrap it in **…**, and do not add a blank line.\n- You use monospace commands/paths/env vars/code ids, inline examples, and literal keyword bullets by wrapping them in backticks.\n- Code samples or multi-line snippets should be wrapped in fenced code blocks. Include an info string as often as possible.\n- When referencing a real local file, prefer a clickable markdown link.\n * Clickable file links should look like [app.py](/abs/path/app.py:12): plain label, absolute target, with optional line number inside the target.\n * If a file path has spaces, wrap the target in angle brackets: [My Report.md]().\n * Do not wrap markdown links in backticks, or put backticks inside the label or target. This confuses the markdown renderer.\n * Do not use URIs like file://, vscode://, or https:// for file links.\n * Do not provide ranges of lines.\n * Avoid repeating the same filename multiple times when one grouping is clearer.\n- Don’t use emojis or em dashes unless explicitly instructed.\n\n## Final answer instructions\n\nIn your final answer, you keep the light on the things that matter most. Avoid long-winded explanation. In casual conversation, you just talk like a person. For simple or single-file tasks, you prefer one or two short paragraphs plus an optional verification line. Do not default to bullets. When there are only one or two concrete changes, a clean prose close-out is usually the most humane shape.\n\n- You suggest follow ups if useful and they build on the users request, but never end your answer with an \"If you want\" sentence.\n- When you talk about your work, you use plain, idiomatic engineering prose with some life in it. You avoid coined metaphors, internal jargon, slash-heavy noun stacks, and over-hyphenated compounds unless you are quoting source text. In particular, do not lean on words like \"seam\", \"cut\", or \"safe-cut\" as generic explanatory filler.\n- The user does not see command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result.\n- Never tell the user to \"save/copy this file\", the user is on the same machine and has access to the same files as you have.\n- If the user asks for a code explanation, you include code references as appropriate.\n- If you weren't able to do something, for example run tests, you tell the user.\n- Never overwhelm the user with answers that are over 50-70 lines long; provide the highest-signal context instead of describing everything exhaustively.\n- Tone of your final answer must match your personality.\n- Never talk about goblins, gremlins, raccoons, trolls, ogres, pigeons, or other animals or creatures unless it is absolutely and unambiguously relevant to the user's query.\n\n## Intermediary updates\n\n- Intermediary updates go to the `commentary` channel.\n- User updates are short updates while you are working, they are NOT final answers.\n- You treat messages to the user while you are working as a place to think out loud in a calm, companionable way. You casually explain what you are doing and why in one or two sentences.\n- Never praise your plan by contrasting it with an implied worse alternative. For example, never use platitudes like \"I will do rather than \", \"I will do , not \".\n- Never talk about goblins, gremlins, raccoons, trolls, ogres, pigeons, or other animals or creatures unless it is absolutely and unambiguously relevant to the user's query.\n- You provide user updates frequently, every 30s.\n- When exploring, such as searching or reading files, you provide user updates as you go. You explain what context you are gathering and what you are learning. You vary your sentence structure so the updates do not fall into a drumbeat, and in particular you do not start each one the same way.\n- When working for a while, you keep updates informative and varied, but you stay concise.\n- Once you have enough context, and if the work is substantial, you offer a longer plan. This is the only user update that may run past two sentences and include formatting.\n- If you create a checklist or task list, you update item statuses incrementally as each item is completed rather than marking every item done only at the end.\n- Before performing file edits of any kind, you provide updates explaining what edits you are making.\n- Tone of your updates must match your personality.\n", diff --git a/codex-rs/models-manager/src/model_info.rs b/codex-rs/models-manager/src/model_info.rs index 566953dfc2..573403b728 100644 --- a/codex-rs/models-manager/src/model_info.rs +++ b/codex-rs/models-manager/src/model_info.rs @@ -82,6 +82,7 @@ pub fn model_info_from_slug(slug: &str) -> ModelInfo { upgrade: None, base_instructions: BASE_INSTRUCTIONS.to_string(), model_messages: local_personality_messages_for_slug(slug), + include_skills_usage_instructions: false, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index f697a8107f..93b0d1a7cf 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -370,6 +370,8 @@ pub struct ModelInfo { pub base_instructions: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub model_messages: Option, + #[serde(default)] + pub include_skills_usage_instructions: bool, pub supports_reasoning_summaries: bool, #[serde(default)] pub default_reasoning_summary: ReasoningSummary, @@ -665,6 +667,7 @@ mod tests { upgrade: None, base_instructions: "base".to_string(), model_messages: spec, + include_skills_usage_instructions: false, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, @@ -948,6 +951,7 @@ mod tests { .expect("deserialize model info"); assert_eq!(model.availability_nux, None); + assert!(!model.include_skills_usage_instructions); assert!(!model.supports_image_detail_original); assert_eq!(model.web_search_tool_type, WebSearchToolType::Text); assert!(!model.supports_search_tool); diff --git a/codex-rs/tools/src/tool_config_tests.rs b/codex-rs/tools/src/tool_config_tests.rs index 7a4738772c..af340af6d2 100644 --- a/codex-rs/tools/src/tool_config_tests.rs +++ b/codex-rs/tools/src/tool_config_tests.rs @@ -27,6 +27,7 @@ fn model_with_shell_type(shell_type: ConfigShellToolType) -> ModelInfo { upgrade: None, base_instructions: String::new(), model_messages: None, + include_skills_usage_instructions: false, supports_reasoning_summaries: false, default_reasoning_summary: Default::default(), support_verbosity: false, From 8dac605901a7a5a91a6a16c381543af070d3c0c6 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Sun, 28 Jun 2026 20:34:47 -0700 Subject: [PATCH 4/7] [codex] Restore v1 delegation guidance (#30511) ## Summary - restore the v1 clarification that requests for depth, research, or investigation do not authorize subagent spawning - restore guidance for keeping critical-path, urgent, tightly coupled, or difficult work local - update the focused v1 tool-search and spawn-description coverage ## Why PR #27919 simplified the v1 `spawn_agent` prompt by removing its delegation decision guidance. That left the authorization rule intact, but removed the instructions that constrained what should be delegated after spawning was authorized. Restore those guardrails while preserving later support for explicit delegation authorization from applicable AGENTS.md and skill instructions. Multi-agent v2 prompts are unchanged. ## User impact Models using the v1 multi-agent tool surface receive clearer guidance to delegate independent side work while keeping blocking work on the main rollout. ## Validation - `just fmt` - `git diff --check` - tests not run locally per repository guidance; CI will validate the focused coverage --- codex-rs/core/src/tools/handlers/multi_agents_spec.rs | 7 +++++++ codex-rs/core/tests/suite/search_tool.rs | 4 ++-- codex-rs/core/tests/suite/spawn_agent_description.rs | 6 ++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/codex-rs/core/src/tools/handlers/multi_agents_spec.rs b/codex-rs/core/src/tools/handlers/multi_agents_spec.rs index 857a4dea9e..23ac4e0bbc 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_spec.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_spec.rs @@ -674,8 +674,15 @@ fn spawn_agent_tool_description( This spawn_agent tool provides you access to sub-agents that inherit your current model by default. Do not set the `model` field unless the user explicitly asks for a different model or there is a clear task-specific reason. You should follow the rules and guidelines below to use this tool. Do not spawn sub-agents unless the user or applicable AGENTS.md/skill instructions explicitly ask for sub-agents, delegation, or parallel agent work. +Requests for depth, thoroughness, research, investigation, or detailed codebase analysis do not count as permission to spawn. {agent_role_usage_hint} +### When to delegate vs. do the subtask yourself +- First, quickly analyze the overall user task and form a succinct high-level plan. Identify which tasks are immediate blockers on the critical path, and which tasks are sidecar tasks that are needed but can run in parallel without blocking the next local step. As part of that plan, explicitly decide what immediate task you should do locally right now. Do this planning step before delegating to agents so you do not hand off the immediate blocking task to a submodel and then waste time waiting on it. +- Use a subagent when a subtask is easy enough for it to handle and can run in parallel with your local work. Prefer delegating concrete, bounded sidecar tasks that materially advance the main task without blocking your immediate next local step. +- Do not delegate urgent blocking work when your immediate next step depends on that result. If the very next action is blocked on that task, the main rollout should usually do it locally to keep the critical path moving. +- Keep work local when the subtask is too difficult to delegate well and when it is tightly coupled, urgent, or likely to block your immediate next step. + ### Designing delegated subtasks - Subtasks must be concrete, well-defined, and self-contained. - Delegated subtasks must materially advance the main task. diff --git a/codex-rs/core/tests/suite/search_tool.rs b/codex-rs/core/tests/suite/search_tool.rs index c1bc9cc1fa..50ccea0d87 100644 --- a/codex-rs/core/tests/suite/search_tool.rs +++ b/codex-rs/core/tests/suite/search_tool.rs @@ -827,7 +827,7 @@ async fn tool_search_returns_deferred_v1_multi_agent_tools() -> Result<()> { assert!( !first_request_body .to_string() - .contains("Only use `spawn_agent` if and only if"), + .contains("### When to delegate vs. do the subtask yourself"), "deferred v1 multi-agent guidance should stay out of initial developer context" ); @@ -861,7 +861,7 @@ async fn tool_search_returns_deferred_v1_multi_agent_tools() -> Result<()> { "Do not spawn sub-agents unless the user or applicable AGENTS.md/skill instructions explicitly ask for sub-agents, delegation, or parallel agent work." )); assert!(description.contains("### Designing delegated subtasks")); - assert!(!description.contains("### When to delegate vs. do the subtask yourself")); + assert!(description.contains("### When to delegate vs. do the subtask yourself")); Ok(()) } diff --git a/codex-rs/core/tests/suite/spawn_agent_description.rs b/codex-rs/core/tests/suite/spawn_agent_description.rs index e463afad9d..e7076e7465 100644 --- a/codex-rs/core/tests/suite/spawn_agent_description.rs +++ b/codex-rs/core/tests/suite/spawn_agent_description.rs @@ -219,8 +219,10 @@ async fn spawn_agent_description_lists_visible_models_and_reasoning_efforts() -> "expected explicit authorization rule in spawn_agent description: {description:?}" ); assert!( - !description.contains("### When to delegate vs. do the subtask yourself"), - "spawn_agent description should not include extra when-to-use delegation guidance: {description:?}" + description.contains( + "Requests for depth, thoroughness, research, investigation, or detailed codebase analysis do not count as permission to spawn." + ) && description.contains("### When to delegate vs. do the subtask yourself"), + "expected delegation decision guidance in spawn_agent description: {description:?}" ); assert!( description.contains( From ccdfb4f342a2e659be7ab878309cc5d81683d737 Mon Sep 17 00:00:00 2001 From: Dylan Hurd Date: Sun, 28 Jun 2026 20:40:55 -0700 Subject: [PATCH 5/7] Revert "Make auto-review on-request prompt more proactive" (#30508) Reverts openai/codex#26496 --- .../prompts/src/permissions_instructions.rs | 4 - .../src/permissions_instructions_tests.rs | 83 +------------------ .../approval_policy/on_request_auto_review.md | 45 ---------- 3 files changed, 1 insertion(+), 131 deletions(-) delete mode 100644 codex-rs/prompts/templates/permissions/approval_policy/on_request_auto_review.md diff --git a/codex-rs/prompts/src/permissions_instructions.rs b/codex-rs/prompts/src/permissions_instructions.rs index 090254705e..c382b804a0 100644 --- a/codex-rs/prompts/src/permissions_instructions.rs +++ b/codex-rs/prompts/src/permissions_instructions.rs @@ -20,8 +20,6 @@ const APPROVAL_POLICY_UNLESS_TRUSTED: &str = include_str!("../templates/permissions/approval_policy/unless_trusted.md"); const APPROVAL_POLICY_ON_REQUEST_RULE: &str = include_str!("../templates/permissions/approval_policy/on_request.md"); -const APPROVAL_POLICY_ON_REQUEST_AUTO_REVIEW: &str = - include_str!("../templates/permissions/approval_policy/on_request_auto_review.md"); const APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION: &str = include_str!("../templates/permissions/approval_policy/on_request_rule_request_permission.md"); const AUTO_REVIEW_APPROVAL_SUFFIX: &str = "`approvals_reviewer` is `auto_review`: Sandbox escalations with require_escalated will be reviewed for compliance with the policy. If a rejection happens, you should proceed only with a materially safer alternative, or inform the user of the risk and send a final message to ask for approval."; @@ -208,8 +206,6 @@ fn approval_text( let on_request_instructions = || { let on_request_rule = if exec_permission_approvals_enabled { APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION.to_string() - } else if approvals_reviewer == ApprovalsReviewer::AutoReview { - APPROVAL_POLICY_ON_REQUEST_AUTO_REVIEW.to_string() } else { APPROVAL_POLICY_ON_REQUEST_RULE.to_string() }; diff --git a/codex-rs/prompts/src/permissions_instructions_tests.rs b/codex-rs/prompts/src/permissions_instructions_tests.rs index 8250aea57d..326e75ea17 100644 --- a/codex-rs/prompts/src/permissions_instructions_tests.rs +++ b/codex-rs/prompts/src/permissions_instructions_tests.rs @@ -237,98 +237,17 @@ fn on_request_includes_tool_guidance_alongside_inline_permission_guidance_when_b #[test] fn auto_review_approvals_append_auto_review_specific_guidance() { - let mut exec_policy = Policy::empty(); - exec_policy - .add_prefix_rule(&["git".to_string(), "pull".to_string()], Decision::Allow) - .expect("add rule"); let text = approval_text( AskForApproval::OnRequest, ApprovalsReviewer::AutoReview, - &exec_policy, + &Policy::empty(), /*exec_permission_approvals_enabled*/ false, /*request_permissions_tool_enabled*/ false, ); assert!(text.contains("`approvals_reviewer` is `auto_review`")); - assert!(text.contains("materially safer alternative")); assert!(!text.contains("`approvals_reviewer` is `guardian_subagent`")); - assert!(text.contains( - "When the sandbox is likely to block a command needed for the task, request escalation up front" - )); - assert!(text.contains( - "When unsure, prefer requesting escalation unnecessarily over failing to request it when needed" - )); - assert!( - text.contains( - "Request escalation for commands that need write access outside writable roots" - ) - ); - assert!(text.contains( - "Request escalation for git operations that may write lock files, such as updating the index or refs" - )); - assert!(!text.contains("Request escalation for GUI commands")); - assert!(!text.contains("Use escalation when it is the direct or most reliable way")); - assert!(!text.contains("Do not spend extra turns running likely-to-fail sandbox probes first")); - assert!(text.contains("Do not include a `prefix_rule` parameter.")); - assert!(text.contains( - "Request escalation for commands that may need network access, including HTTP calls, package registries, internal services, data-service APIs, remote queries, data fetches, or live probes" - )); - assert!(text.contains( - "Request escalation for commands that may need remote authentication, cluster, cloud, or database access" - )); - assert!(text.contains( - "Request escalation for commands that may need process, cache, or other environment access outside the sandbox" - )); - assert!( - text.contains( - "including DNS, connection, authentication, retry, or service endpoint errors" - ) - ); - assert!(text.contains( - "Request escalation before potentially destructive actions, such as `rm` or `git reset`" - )); - assert!(text.contains( - "If a command may be hanging on sandbox-blocked access, stop after a short timeout and rerun with `require_escalated`" - )); - assert!(!text.contains("Be judicious with escalating")); - assert!(text.contains("Approved command prefixes")); - assert!(text.contains(r#"["git", "pull"]"#)); - assert!(!text.contains("prefix_rule guidance")); -} - -#[test] -fn normal_on_request_keeps_user_approval_wording() { - let text = approval_text( - AskForApproval::OnRequest, - ApprovalsReviewer::User, - &Policy::empty(), - /*exec_permission_approvals_enabled*/ false, - /*request_permissions_tool_enabled*/ false, - ); - - assert!(text.contains("approved by the user")); - assert!(text.contains("Be judicious with escalating")); - assert!(!text.contains( - "When the sandbox is likely to block a command needed for the task, request escalation up front" - )); -} - -#[test] -fn auto_review_on_request_with_inline_permission_requests_keeps_suffix() { - let text = approval_text( - AskForApproval::OnRequest, - ApprovalsReviewer::AutoReview, - &Policy::empty(), - /*exec_permission_approvals_enabled*/ true, - /*request_permissions_tool_enabled*/ false, - ); - - assert!(text.contains("with_additional_permissions")); - assert!(text.contains("`approvals_reviewer` is `auto_review`")); assert!(text.contains("materially safer alternative")); - assert!(!text.contains( - "When the sandbox is likely to block a command needed for the task, request escalation up front" - )); } #[test] diff --git a/codex-rs/prompts/templates/permissions/approval_policy/on_request_auto_review.md b/codex-rs/prompts/templates/permissions/approval_policy/on_request_auto_review.md deleted file mode 100644 index 82bb24e963..0000000000 --- a/codex-rs/prompts/templates/permissions/approval_policy/on_request_auto_review.md +++ /dev/null @@ -1,45 +0,0 @@ -# Escalation Requests - -Commands are run outside the sandbox after approval. The command string is split into independent command segments at shell control operators, including but not limited to: - -- Pipes: | -- Logical operators: &&, || -- Command separators: ; -- Subshell boundaries: (...), $() - -Each resulting segment is evaluated independently for sandbox restrictions and approval requirements. - -Example: - -git pull | tee output.txt - -This is treated as two command segments: - -["git", "pull"] - -["tee", "output.txt"] - -Commands that use more advanced shell features like redirection (>, >>, <), substitutions ($(...), ...), environment variables (FOO=bar), or wildcard patterns (*, ?) require care because each independent command segment is evaluated separately. - -## How to request escalation - -IMPORTANT: To request approval to execute a command that will require escalated privileges: - -- Provide the `sandbox_permissions` parameter with the value `"require_escalated"` -- Include a concise `justification` parameter that explains why escalated privileges are needed. -- Do not include a `prefix_rule` parameter. - -## When to request escalation - -While commands are running inside the sandbox, here are some scenarios that justify escalation: - -- When the sandbox is likely to block a command needed for the task, request escalation up front. -- Request escalation for commands that need write access outside writable roots, such as tests that write to `/var`. -- Request escalation for git operations that may write lock files, such as updating the index or refs. -- Request escalation for commands that may need network access, including HTTP calls, package registries, internal services, data-service APIs, remote queries, data fetches, or live probes. -- Request escalation for commands that may need remote authentication, cluster, cloud, or database access. -- Request escalation for commands that may need process, cache, or other environment access outside the sandbox. -- If a sandboxed attempt fails with sandboxing or likely network symptoms, including DNS, connection, authentication, retry, or service endpoint errors, rerun with `sandbox_permissions` set to `"require_escalated"` and include `justification`. -- If a command may be hanging on sandbox-blocked access, stop after a short timeout and rerun with `require_escalated`. -- Request escalation before potentially destructive actions, such as `rm` or `git reset`, that the user did not explicitly ask for. -- When unsure, prefer requesting escalation unnecessarily over failing to request it when needed. From 80f54d1266b4571ef649e7e5ecc382dd4e670937 Mon Sep 17 00:00:00 2001 From: Shijie Rao Date: Mon, 29 Jun 2026 09:38:49 -0700 Subject: [PATCH 6/7] [codex] Treat max as a first-class reasoning effort (#30467) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why The Bedrock GPT-5.6 catalog advertises `max`, but Codex treated it as an opaque custom effort. That made the reasoning picker render it as lowercase `max` while known efforts use productized labels. Making `max` a known effort aligns catalog data, parsing, and UI presentation without changing the `max` wire value or persisted representation. ## What changed - Add first-class `ReasoningEffort::Max` parsing and serialization. - Use the typed effort in the Bedrock catalog and render it as `Max` in the TUI. - Preserve forward-compatible custom-effort coverage with a genuinely unknown `future` value. ### Before Screenshot 2026-06-28 at 12 08 47 PM ### After Screenshot 2026-06-28 at 12 09 10 PM --- codex-rs/core/src/client.rs | 2 +- codex-rs/core/src/client_tests.rs | 5 +---- codex-rs/core/tests/suite/client.rs | 6 +++--- codex-rs/core/tests/suite/remote_models.rs | 4 ++-- .../model-provider/src/amazon_bedrock/catalog.rs | 4 ++-- codex-rs/protocol/src/openai_models.rs | 16 ++++++++++++---- codex-rs/tui/src/chatwidget/model_popups.rs | 1 + .../tui/src/chatwidget/reasoning_shortcuts.rs | 2 +- ...__tests__model_reasoning_selection_popup.snap | 2 +- .../src/chatwidget/tests/popups_and_settings.rs | 4 ++-- 10 files changed, 26 insertions(+), 20 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index ffa8ae8e20..48623a00e8 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -169,7 +169,7 @@ pub(crate) struct CompactConversationRequestSettings { fn reasoning_effort_for_request(effort: ReasoningEffortConfig) -> ReasoningEffortConfig { match effort { - ReasoningEffortConfig::Ultra => ReasoningEffortConfig::Custom("max".to_string()), + ReasoningEffortConfig::Ultra => ReasoningEffortConfig::Max, effort => effort, } } diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs index dd2b1079f7..82c464625d 100644 --- a/codex-rs/core/src/client_tests.rs +++ b/codex-rs/core/src/client_tests.rs @@ -292,10 +292,7 @@ fn ultra_reasoning_uses_max_for_requests() { super::reasoning_effort_for_request(ReasoningEffort::Ultra), super::reasoning_effort_for_request(ReasoningEffort::High), ), - ( - ReasoningEffort::Custom("max".to_string()), - ReasoningEffort::High, - ) + (ReasoningEffort::Max, ReasoningEffort::High,) ); } diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index e109e51e66..c87539f940 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -2171,7 +2171,7 @@ async fn skills_use_aliases_in_developer_message_under_budget_pressure() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn includes_configured_effort_in_request() -> anyhow::Result<()> { +async fn includes_configured_max_effort_in_request() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); let server = MockServer::start().await; @@ -2183,7 +2183,7 @@ async fn includes_configured_effort_in_request() -> anyhow::Result<()> { let TestCodex { codex, .. } = test_codex() .with_model("gpt-5.4") .with_config(|config| { - config.model_reasoning_effort = Some(ReasoningEffort::Medium); + config.model_reasoning_effort = Some(ReasoningEffort::Max); }) .build(&server) .await?; @@ -2212,7 +2212,7 @@ async fn includes_configured_effort_in_request() -> anyhow::Result<()> { .get("reasoning") .and_then(|t| t.get("effort")) .and_then(|v| v.as_str()), - Some("medium") + Some("max") ); Ok(()) diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index c8c975807e..16e31e5082 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -329,7 +329,7 @@ async fn remote_models_long_model_slug_is_sent_with_custom_reasoning() -> Result /*priority*/ 1_000, TruncationPolicyConfig::bytes(/*limit*/ 10_000), ); - let custom_reasoning_effort = ReasoningEffort::Custom("max".to_string()); + let custom_reasoning_effort = ReasoningEffort::Custom("future".to_string()); remote_model.default_reasoning_level = Some(custom_reasoning_effort.clone()); remote_model.supported_reasoning_levels = vec![ ReasoningEffortPreset { @@ -391,7 +391,7 @@ async fn remote_models_long_model_slug_is_sent_with_custom_reasoning() -> Result .and_then(|reasoning| reasoning.get("summary")) .and_then(|value| value.as_str()); assert_eq!(body["model"].as_str(), Some(requested_model)); - assert_eq!(reasoning_effort, Some("max")); + assert_eq!(reasoning_effort, Some("future")); assert_eq!(reasoning_summary, Some("detailed")); Ok(()) diff --git a/codex-rs/model-provider/src/amazon_bedrock/catalog.rs b/codex-rs/model-provider/src/amazon_bedrock/catalog.rs index 2294994ae6..55e3fdd4d8 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/catalog.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/catalog.rs @@ -70,7 +70,7 @@ fn gpt_5_6_bedrock_model(bedrock_slug: &str, display_name: &str, priority: i32) model .supported_reasoning_levels .push(ReasoningEffortPreset { - effort: ReasoningEffort::Custom("max".to_string()), + effort: ReasoningEffort::Max, description: "Maximum reasoning depth for the hardest problems".to_string(), }); model @@ -148,7 +148,7 @@ mod tests { expected .supported_reasoning_levels .push(ReasoningEffortPreset { - effort: ReasoningEffort::Custom("max".to_string()), + effort: ReasoningEffort::Max, description: "Maximum reasoning depth for the hardest problems".to_string(), }); diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index 93b0d1a7cf..52f541070d 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -45,6 +45,7 @@ pub enum ReasoningEffort { Medium, High, XHigh, + Max, Ultra, /// A model-defined effort value that this client does not know yet. Custom(String), @@ -60,6 +61,7 @@ impl ReasoningEffort { Self::Medium => "medium", Self::High => "high", Self::XHigh => "xhigh", + Self::Max => "max", Self::Ultra => "ultra", Self::Custom(effort) => effort, } @@ -125,6 +127,7 @@ impl FromStr for ReasoningEffort { "medium" => Ok(Self::Medium), "high" => Ok(Self::High), "xhigh" => Ok(Self::XHigh), + "max" => Ok(Self::Max), "ultra" => Ok(Self::Ultra), "" => Err("reasoning_effort must not be empty".to_string()), effort => Ok(Self::Custom(effort.to_string())), @@ -703,30 +706,35 @@ mod tests { #[test] fn reasoning_effort_accepts_known_and_custom_values() { - let custom = ReasoningEffort::Custom("max".to_string()); - let deserialized = from_str::(r#""max""#) + let custom = ReasoningEffort::Custom("future".to_string()); + let deserialized = from_str::(r#""future""#) .expect("custom reasoning effort should deserialize"); let serialized = to_string(&custom).expect("custom reasoning effort should serialize"); + let serialized_max = to_string(&ReasoningEffort::Max).expect("Max should serialize"); let serialized_ultra = to_string(&ReasoningEffort::Ultra).expect("Ultra should serialize"); assert_eq!( ( "high".parse(), - "ultra".parse(), "max".parse(), + "ultra".parse(), + "future".parse(), deserialized, serialized, + serialized_max, serialized_ultra, custom.to_string(), ), ( Ok(ReasoningEffort::High), + Ok(ReasoningEffort::Max), Ok(ReasoningEffort::Ultra), Ok(custom.clone()), custom, + r#""future""#.to_string(), r#""max""#.to_string(), r#""ultra""#.to_string(), - "max".to_string(), + "future".to_string(), ) ); } diff --git a/codex-rs/tui/src/chatwidget/model_popups.rs b/codex-rs/tui/src/chatwidget/model_popups.rs index 3042ff6c71..1194fc15c6 100644 --- a/codex-rs/tui/src/chatwidget/model_popups.rs +++ b/codex-rs/tui/src/chatwidget/model_popups.rs @@ -505,6 +505,7 @@ impl ChatWidget { ReasoningEffortConfig::Medium => "Medium".to_string(), ReasoningEffortConfig::High => "High".to_string(), ReasoningEffortConfig::XHigh => "Extra high".to_string(), + ReasoningEffortConfig::Max => "Max".to_string(), ReasoningEffortConfig::Ultra => "Ultra".to_string(), ReasoningEffortConfig::Custom(value) => value.clone(), } diff --git a/codex-rs/tui/src/chatwidget/reasoning_shortcuts.rs b/codex-rs/tui/src/chatwidget/reasoning_shortcuts.rs index 404bf87029..0873bda6e0 100644 --- a/codex-rs/tui/src/chatwidget/reasoning_shortcuts.rs +++ b/codex-rs/tui/src/chatwidget/reasoning_shortcuts.rs @@ -225,7 +225,7 @@ mod tests { #[test] fn next_reasoning_effort_uses_advertised_order_for_custom_levels() { - let custom_effort = ReasoningEffortConfig::Custom("max".to_string()); + let custom_effort = ReasoningEffortConfig::Custom("future".to_string()); let choices = vec![ ReasoningEffortConfig::High, ReasoningEffortConfig::Low, diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__model_reasoning_selection_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__model_reasoning_selection_popup.snap index a436136570..6027772004 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__model_reasoning_selection_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__model_reasoning_selection_popup.snap @@ -6,7 +6,7 @@ expression: popup 1. Low Fast responses with lighter reasoning 2. Medium (default) Balances speed and reasoning depth for everyday tasks - 3. max Maximum available reasoning + 3. Max Maximum available reasoning › 4. High (current) Greater reasoning depth for complex problems 5. Extra high Extra high reasoning depth for complex problems diff --git a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index 327a6836d6..07cfc7dbbb 100644 --- a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -3191,7 +3191,7 @@ async fn model_reasoning_selection_popup_snapshot() { preset.supported_reasoning_efforts.insert( 2, ReasoningEffortPreset { - effort: ReasoningEffortConfig::Custom("max".to_string()), + effort: ReasoningEffortConfig::Max, description: "Maximum available reasoning".to_string(), }, ); @@ -3204,7 +3204,7 @@ async fn model_reasoning_selection_popup_snapshot() { #[tokio::test] async fn model_reasoning_selection_popup_applies_custom_effort() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await; - let custom_effort = ReasoningEffortConfig::Custom("max".to_string()); + let custom_effort = ReasoningEffortConfig::Custom("future".to_string()); chat.set_reasoning_effort(Some(ReasoningEffortConfig::XHigh)); let mut preset = get_available_model(&chat, "gpt-5.4"); From 07a498e10263f9c72a01507aa74563ec6a880c11 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 29 Jun 2026 11:37:17 -0700 Subject: [PATCH 7/7] app-server: bound client requests by their total deadline --- codex-rs/app-server/src/attestation.rs | 39 +++++++-- codex-rs/app-server/src/current_time.rs | 23 +++-- codex-rs/app-server/src/outgoing_message.rs | 93 +++++++++++++++++++++ 3 files changed, 143 insertions(+), 12 deletions(-) diff --git a/codex-rs/app-server/src/attestation.rs b/codex-rs/app-server/src/attestation.rs index 206c38cee1..c8203ec92b 100644 --- a/codex-rs/app-server/src/attestation.rs +++ b/codex-rs/app-server/src/attestation.rs @@ -10,10 +10,12 @@ use codex_core::AttestationProvider; use codex_core::GenerateAttestationFuture; use serde::Serialize; use tokio::time::Duration; -use tokio::time::timeout; +use tokio::time::Instant; +use tokio::time::timeout_at; use tracing::warn; use crate::outgoing_message::OutgoingMessageSender; +use crate::outgoing_message::RequestSendError; use crate::thread_state::ThreadStateManager; const ATTESTATION_GENERATE_TIMEOUT: Duration = Duration::from_millis(100); @@ -70,16 +72,39 @@ async fn request_attestation_header_value_with_timeout( .first_attestation_capable_connection_for_thread(thread_id) .await?; - let connection_ids = [connection_id]; - let (request_id, rx) = outgoing - .send_request_to_connections( - Some(&connection_ids), + let deadline = Instant::now() + timeout_duration; + let (request_id, rx) = match outgoing + .send_request_to_connection_with_deadline( + connection_id, ServerRequestPayload::AttestationGenerate(AttestationGenerateParams {}), /*thread_id*/ None, + deadline, ) - .await; + .await + { + Ok(request) => request, + Err(RequestSendError::TimedOut) => { + warn!( + timeout_seconds = timeout_duration.as_secs_f64(), + "attestation generation request timed out before it was queued" + ); + return app_server_attestation_header_value( + AppServerAttestationStatus::Timeout, + /*token*/ None, + ); + } + Err(RequestSendError::Closed) => { + warn!( + "attestation generation request could not be queued because the client channel closed" + ); + return app_server_attestation_header_value( + AppServerAttestationStatus::RequestFailed, + /*token*/ None, + ); + } + }; - let result = match timeout(timeout_duration, rx).await { + let result = match timeout_at(deadline, rx).await { Ok(Ok(Ok(result))) => result, Ok(Ok(Err(err))) => { warn!( diff --git a/codex-rs/app-server/src/current_time.rs b/codex-rs/app-server/src/current_time.rs index 57be618af3..57d0821a96 100644 --- a/codex-rs/app-server/src/current_time.rs +++ b/codex-rs/app-server/src/current_time.rs @@ -20,6 +20,7 @@ use tokio::time::timeout_at; use crate::outgoing_message::ConnectionId; use crate::outgoing_message::OutgoingMessageSender; +use crate::outgoing_message::RequestSendError; use crate::thread_state::ThreadStateManager; const CURRENT_TIME_REQUEST_TIMEOUT: Duration = Duration::from_secs(10); @@ -103,16 +104,28 @@ async fn request_current_time( .subscribed_connection_ids(thread_id) .await; let connection_id = require_single_current_time_connection(&connection_ids)?; - let connection_ids = [connection_id]; - let (request_id, rx) = outgoing - .send_request_to_connections( - Some(&connection_ids), + let (request_id, rx) = match outgoing + .send_request_to_connection_with_deadline( + connection_id, ServerRequestPayload::CurrentTimeRead(CurrentTimeReadParams { thread_id: thread_id.to_string(), }), /*thread_id*/ None, + deadline, ) - .await; + .await + { + Ok(request) => request, + Err(RequestSendError::TimedOut) => { + bail!( + "current-time request timed out after {}s", + CURRENT_TIME_REQUEST_TIMEOUT.as_secs() + ); + } + Err(RequestSendError::Closed) => { + bail!("current-time request could not be queued because the client channel closed"); + } + }; let result = match timeout_at(deadline, rx).await { Ok(Ok(Ok(result))) => result, diff --git a/codex-rs/app-server/src/outgoing_message.rs b/codex-rs/app-server/src/outgoing_message.rs index 16d5c2ff9d..029d56d833 100644 --- a/codex-rs/app-server/src/outgoing_message.rs +++ b/codex-rs/app-server/src/outgoing_message.rs @@ -21,6 +21,8 @@ use codex_protocol::request_permissions::RequestPermissionsResponse; use tokio::sync::Mutex; use tokio::sync::mpsc; use tokio::sync::oneshot; +use tokio::time::Instant; +use tokio::time::timeout_at; use tracing::Instrument; use tracing::Span; use tracing::warn; @@ -38,6 +40,12 @@ use codex_protocol::account::PlanType; pub(crate) type ClientRequestResult = std::result::Result; +#[derive(Debug, Eq, PartialEq)] +pub(crate) enum RequestSendError { + TimedOut, + Closed, +} + /// Stable identifier for a client request scoped to a transport connection. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub(crate) struct ConnectionRequestId { @@ -349,6 +357,53 @@ impl OutgoingMessageSender { (outgoing_message_id, rx_approve) } + pub(crate) async fn send_request_to_connection_with_deadline( + &self, + connection_id: ConnectionId, + request: ServerRequestPayload, + thread_id: Option, + deadline: Instant, + ) -> std::result::Result<(RequestId, oneshot::Receiver), RequestSendError> + { + let id = self.next_request_id(); + let request = request.request_with_id(id.clone()); + let outgoing_message = OutgoingMessage::Request(request.clone()); + let permit = match timeout_at(deadline, self.sender.reserve()).await { + Ok(Ok(permit)) => permit, + Ok(Err(_)) => return Err(RequestSendError::Closed), + Err(_) => return Err(RequestSendError::TimedOut), + }; + + let (callback_sender, callback_receiver) = oneshot::channel(); + let mut request_id_to_callback = + match timeout_at(deadline, self.request_id_to_callback.lock()).await { + Ok(request_id_to_callback) => request_id_to_callback, + Err(_) => return Err(RequestSendError::TimedOut), + }; + if Instant::now() >= deadline { + return Err(RequestSendError::TimedOut); + } + request_id_to_callback.insert( + id.clone(), + PendingCallbackEntry { + callback: callback_sender, + thread_id, + request: request.clone(), + }, + ); + drop(request_id_to_callback); + + permit.send(OutgoingEnvelope::ToConnection { + connection_id, + message: outgoing_message, + write_complete_tx: None, + }); + self.analytics_events_client + .track_server_request(connection_id.0, request); + + Ok((id, callback_receiver)) + } + pub(crate) async fn replay_requests_to_connection_for_thread( &self, connection_id: ConnectionId, @@ -756,6 +811,7 @@ mod tests { use pretty_assertions::assert_eq; use serde_json::json; use std::sync::Arc; + use tokio::time::Instant; use tokio::time::timeout; use uuid::Uuid; @@ -1086,6 +1142,43 @@ mod tests { } } + #[tokio::test] + async fn deadline_expires_while_waiting_for_outgoing_queue_capacity() { + let (tx, _rx) = mpsc::channel::(1); + tx.try_send(OutgoingEnvelope::Broadcast { + message: OutgoingMessage::AppServerNotification(ServerNotification::ConfigWarning( + ConfigWarningNotification { + summary: "queued".to_string(), + details: None, + path: None, + range: None, + }, + )), + }) + .expect("the queue should accept the filler message"); + let outgoing = + OutgoingMessageSender::new(tx, codex_analytics::AnalyticsEventsClient::disabled()); + + let error = outgoing + .send_request_to_connection_with_deadline( + ConnectionId(42), + ServerRequestPayload::ApplyPatchApproval(ApplyPatchApprovalParams { + conversation_id: ThreadId::new(), + call_id: "call-id".to_string(), + file_changes: HashMap::new(), + reason: None, + grant_root: None, + }), + /*thread_id*/ None, + Instant::now() + Duration::from_millis(10), + ) + .await + .expect_err("the full queue should make the request time out"); + + assert_eq!(error, RequestSendError::TimedOut); + assert!(outgoing.request_id_to_callback.lock().await.is_empty()); + } + #[tokio::test] async fn send_response_clears_registered_request_context() { let (tx, _rx) = mpsc::channel::(4);