From fdeaa6c1ecad21ae5e154e5873d166488b1fbd5e Mon Sep 17 00:00:00 2001 From: pmccrary-oai Date: Wed, 23 Sep 2026 18:14:43 +0000 Subject: [PATCH] Allow history and notes without experimental context capability (#47625) ## Why Enabling `features.token_budget.use_history_notes_extension`, explicitly or through model defaults, rejected session startup when the model lacked `supports_experimental_context`, even with experimental mode disabled. ## What changed Remove that startup restriction so token-budget history and notes can be used independently of the model's experimental context capability. ## Testing Extend core tests to cover explicit configuration and model defaults on startup and cold resume, checking `new_context`, token-budget contexts, and `history_ingest_requested` metadata. Add app-server coverage for native history and notes tools and context-window hints without experimental context capability. GitOrigin-RevId: 6157479aeb75ffd785b8d4b2f48b11c6f46e8f35 --- .../tests/suite/v2/history_notes_extension.rs | 14 ++-- codex-rs/core/src/session/mod.rs | 10 --- codex-rs/core/tests/suite/token_budget.rs | 76 ++++++++++++------- 3 files changed, 56 insertions(+), 44 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/history_notes_extension.rs b/codex-rs/app-server/tests/suite/v2/history_notes_extension.rs index a805eeaa61..177590a6a2 100644 --- a/codex-rs/app-server/tests/suite/v2/history_notes_extension.rs +++ b/codex-rs/app-server/tests/suite/v2/history_notes_extension.rs @@ -30,13 +30,15 @@ const THREAD_HINT: &str = "Recent notes (up to 5, most-recent first):\n- /root/notes/latest.md (2 lines, 14 UTF-8 bytes)"; const BRIDGE_HINT: &str = "unstructured notes/thread_hint fixture result"; -#[test_case(true, 200, THREAD_HINT; "native_hint")] -#[test_case(true, 200, ""; "no_notes")] -#[test_case(true, 503, THREAD_HINT; "native_failure_does_not_use_bridge")] -#[test_case(false, 200, THREAD_HINT; "bridge_hint")] +#[test_case(true, true, 200, THREAD_HINT; "native_hint")] +#[test_case(true, false, 200, THREAD_HINT; "native_hint_without_experimental_capability")] +#[test_case(true, true, 200, ""; "no_notes")] +#[test_case(true, true, 503, THREAD_HINT; "native_failure_does_not_use_bridge")] +#[test_case(false, true, 200, THREAD_HINT; "bridge_hint")] #[tokio::test] async fn app_server_uses_configured_notes_backend_for_context_window_hints( use_history_notes_extension: bool, + supports_experimental_context: bool, hint_status: u16, hint_text: &str, ) -> Result<()> { @@ -115,7 +117,7 @@ async fn app_server_uses_configured_notes_backend_for_context_window_hints( let codex_home = TempDir::new()?; let config = load_default_config_for_test(&codex_home).await; let mut model = codex_core::test_support::construct_model_info_offline("mock-model", &config); - model.supports_experimental_context = true; + model.supports_experimental_context = supports_experimental_context; let catalog_path = codex_home.path().join("models.json"); std::fs::write( &catalog_path, @@ -132,7 +134,7 @@ async fn app_server_uses_configured_notes_backend_for_context_window_hints( .with_provider_base_url(&format!("{}/backend-api/codex", server.uri())) .with_provider_config("supports_websockets = false\nrequires_openai_auth = true") .with_extra_config(&format!( - "[features.token_budget]\nenabled = true\nuse_history_notes_extension = {use_history_notes_extension}\n\n[mcp_servers.notes]\nurl = \"{}/mcp\"\nstartup_timeout_sec = 10\n", + "[features.context_management]\nexperimental_mode = false\n\n[features.token_budget]\nenabled = true\nuse_history_notes_extension = {use_history_notes_extension}\n\n[mcp_servers.notes]\nurl = \"{}/mcp\"\nstartup_timeout_sec = 10\n", server.uri(), )) .write(codex_home.path())?; diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 961f7b5d47..22009d3f29 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -726,16 +726,6 @@ impl Session { &model_info, )?; token_budget::apply_model_defaults(Arc::make_mut(&mut config), &model_info); - if config - .token_budget - .as_ref() - .is_some_and(|token_budget| token_budget.use_history_notes_extension) - && !model_info.supports_experimental_context - { - return Err(CodexErr::InvalidRequest(format!( - "features.token_budget.use_history_notes_extension is not supported by model `{model}`; disable it or select a model that supports experimental context" - ))); - } } let configured_config = Arc::clone(&config); let multi_agent_version = config.multi_agent_version_override().or_else(|| { diff --git a/codex-rs/core/tests/suite/token_budget.rs b/codex-rs/core/tests/suite/token_budget.rs index b4eb97390d..6758e64910 100644 --- a/codex-rs/core/tests/suite/token_budget.rs +++ b/codex-rs/core/tests/suite/token_budget.rs @@ -364,14 +364,14 @@ async fn experimental_context_requires_capable_model_and_codex_backend( Ok(()) } -#[test_case(false, true, false; "explicit_history_notes_unsupported")] -#[test_case(false, true, true; "explicit_history_notes_supported")] +#[test_case(false, true, false; "explicit_history_notes_without_experimental_capability")] +#[test_case(false, true, true; "explicit_history_notes_with_experimental_capability")] #[test_case(false, false, false; "explicit_standalone_token_budget")] -#[test_case(true, true, false; "model_default_history_notes_unsupported")] -#[test_case(true, true, true; "model_default_history_notes_supported")] +#[test_case(true, true, false; "model_default_history_notes_without_experimental_capability")] +#[test_case(true, true, true; "model_default_history_notes_with_experimental_capability")] #[test_case(true, false, false; "model_default_standalone_token_budget")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn token_budget_history_notes_requires_capable_starting_model( +async fn token_budget_history_notes_can_be_enabled_without_experimental_context( use_model_defaults: bool, use_history_notes: bool, supports_context: bool, @@ -380,7 +380,7 @@ async fn token_budget_history_notes_requires_capable_starting_model( let server = start_mock_server().await; let backend_url = format!("{}/backend-api/codex", server.uri()); - let result = test_codex() + let mut builder = test_codex() .with_auth(CodexAuth::from_external_chatgpt_tokens( "header.e30.signature", "account-123", @@ -411,30 +411,50 @@ async fn token_budget_history_notes_requires_capable_starting_model( .with_config(move |config| { config.model_provider.base_url = Some(backend_url); config.model_context_window = Some(CONFIGURED_CONTEXT_WINDOW); - }) - .build_with_auto_env(&server) - .await; + }); + let test = builder.build_with_auto_env(&server).await?; - if use_history_notes && !supports_context { - let error = result - .err() - .expect("unsupported history/notes must fail at startup"); - assert!(error.to_string().contains( - "features.token_budget.use_history_notes_extension is not supported by model `gpt-5.2`" - )); - return Ok(()); - } - - let test = result?; - let response = mount_sse_once(&server, sse_completed("resp-1")).await; + let response = mount_sse_sequence( + &server, + vec![sse_completed("resp-1"), sse_completed("resp-2")], + ) + .await; test.submit_turn("inspect token-budget activation").await?; - let request = response.single_request(); - assert!( - tool_names(&request) - .iter() - .any(|name| name == "new_context") - ); - assert_eq!(token_budget_contexts(&request).len(), 1); + // Builder overrides are consumed by the initial build. Reapply the model and + // provider settings so the cold resume exercises the same activation defaults. + let initial_config = test.config.clone(); + let resumed = builder + .with_config(move |config| { + config.model = initial_config.model; + config.model_catalog = initial_config.model_catalog; + config.model_provider = initial_config.model_provider; + config.model_context_window = initial_config.model_context_window; + }) + .restart(&server, &test) + .await?; + resumed + .submit_turn("inspect token-budget activation after restart") + .await?; + + let requests = response.requests(); + assert_eq!(requests.len(), 2); + for request in requests { + assert!( + tool_names(&request) + .iter() + .any(|name| name == "new_context") + ); + assert_eq!(token_budget_contexts(&request).len(), 1); + let turn_metadata: Value = serde_json::from_str( + &request + .header("x-codex-turn-metadata") + .expect("request should include turn metadata"), + )?; + assert_eq!( + turn_metadata["history_ingest_requested"].as_bool(), + use_history_notes.then_some(true) + ); + } Ok(()) }