From aa88a0333c751711901ce078f4b272a6a970f9b9 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Wed, 9 Sep 2026 17:25:48 +0000 Subject: [PATCH] Preserve tool output truncation budgets across resume and fork (#44248) ## Why Replaying tool outputs under a different model can expand or shrink the history shown to the model if truncation uses the new model's budget. ## What changed Save the originating history truncation budget on function and custom tool outputs and reuse it during replay, preserving existing tool-specific overrides. Include the existing 20% serialization allowance once, before converting byte budgets to tokens. Rename the metadata field to `history_truncation_token_limit` while retaining `fallback_token_limit_override` as its serialized name for compatibility. ## Testing Add regression coverage for resume and fork with different model budgets, including custom tool outputs. Extend unit coverage for existing overrides and byte-budget conversion. GitOrigin-RevId: a62c8fd3e43198e2252b150fba72f022939c1070 --- .../app-server-exports-stable.json.zst | Bin 148447 -> 148439 bytes .../tests/suite/v2/thread_inject_items.rs | 10 +- codex-rs/core/src/context_manager/history.rs | 5 +- .../core/src/context_manager/history_tests.rs | 4 +- codex-rs/core/src/session/mod.rs | 19 +- codex-rs/core/src/session/tests.rs | 68 +++++-- codex-rs/core/src/tools/context.rs | 7 +- .../core/src/tools/handlers/mcp_resource.rs | 3 +- codex-rs/core/src/tools/registry.rs | 2 +- codex-rs/core/src/tools/registry_tests.rs | 2 +- codex-rs/core/tests/suite/step_settings.rs | 186 +++++++++++++++++- codex-rs/history/src/lib.rs | 12 +- codex-rs/history/src/tests.rs | 4 +- codex-rs/tools/src/tool_call.rs | 6 +- codex-rs/utils/output-truncation/src/lib.rs | 6 + 15 files changed, 287 insertions(+), 47 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index e6b15206e8a20427f370dcadfd68269014146fbf..41a1f67de25f797718b6b077a919a07036e0967c 100644 GIT binary patch delta 19440 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

5_ZwdmeL5nB|v_p*^*SeA80mDR|TLap4l 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 diff --git a/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs b/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs index 5dfd005b8d..972cf70347 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs @@ -152,7 +152,7 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history( envelope .metadata .as_ref() - .map(|metadata| metadata.client_authored), + .is_some_and(|metadata| metadata.client_authored), )), _ => None, }) @@ -166,10 +166,10 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history( assert_eq!( persisted_injected_items, vec![ - (injected_item.clone(), None), - (injected_developer_item.clone(), Some(true)), - (marker_shaped_developer_item.clone(), Some(true)), - (named_tool_item, None), + (injected_item.clone(), false), + (injected_developer_item.clone(), true), + (marker_shaped_developer_item.clone(), true), + (named_tool_item, false), ] ); diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index ee077e315a..799b9eb6a7 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -57,6 +57,7 @@ use codex_utils_output_truncation::approx_bytes_for_tokens; use codex_utils_output_truncation::approx_token_count; use codex_utils_output_truncation::approx_tokens_from_byte_count_i64; use codex_utils_output_truncation::truncate_function_output_payload; +use codex_utils_output_truncation::with_serialization_allowance; use std::num::NonZeroUsize; use std::ops::Deref; use std::sync::Arc; @@ -366,9 +367,9 @@ impl ContextManager { { // The override already includes the tool's serialization allowance. let policy = metadata - .and_then(|metadata| metadata.fallback_token_limit_override) + .and_then(|metadata| metadata.history_truncation_token_limit) .map(TruncationPolicy::Tokens) - .unwrap_or(policy * 1.2); + .unwrap_or_else(|| with_serialization_allowance(policy)); truncate_function_output_payload(output, policy, estimate_audio_token_count); } if let Some(review_history) = &mut self.review_history diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index bfbe5d162b..fb43d1f1b5 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -659,7 +659,7 @@ fn annotated_history_apis_preserve_envelopes() { #[test_case(Some(100), 85, 100, true; "saved limit has no additional allowance")] #[test_case(Some(30_000), 20_000, 30_000, false; "large explicit budget")] fn record_annotated_items_preserves_metadata_while_processing_item( - fallback_token_limit_override: Option, + history_truncation_token_limit: Option, repeat_count: usize, expected_token_limit: usize, expected_truncation: bool, @@ -677,7 +677,7 @@ fn record_annotated_items_preserves_metadata_while_processing_item( internal_chat_message_metadata_passthrough: None, }, metadata: Some(CodexHarnessMetadata { - fallback_token_limit_override, + history_truncation_token_limit, ..Default::default() }), }; diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 15d24f4129..bdd8f24c64 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -172,6 +172,7 @@ use codex_thread_store::ThreadPersistenceMetadata; use codex_thread_store::ThreadStore; use codex_utils_audio::prepare_response_items as prepare_audio_response_items; use codex_utils_git_discovery::GitRootDiscovery; +use codex_utils_output_truncation::with_serialization_allowance; use codex_utils_path_uri::PathUri; use futures::future::BoxFuture; use futures::future::Shared; @@ -3429,9 +3430,25 @@ impl Session { &self, turn_context: &TurnContext, model_info: &ModelInfo, - items: Vec, + mut items: Vec, image_preparations: Vec, ) { + // Save the originating history budget for replay. + // Preserve any existing tool-specific override. + let policy: codex_utils_output_truncation::TruncationPolicy = + model_info.truncation_policy.into(); + for envelope in &mut items { + if matches!( + envelope.item, + ResponseItem::FunctionCallOutput { .. } | ResponseItem::CustomToolCallOutput { .. } + ) { + envelope + .metadata + .get_or_insert_default() + .history_truncation_token_limit + .get_or_insert_with(|| with_serialization_allowance(policy).token_budget()); + } + } let response_items = items .iter() .map(|envelope| envelope.item.clone()) diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index b36f4329a9..b0e69affe8 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -2532,10 +2532,17 @@ async fn record_inter_agent_communication_preserves_item_id_in_rollout_and_resum ); } -#[test_case::test_case(false; "plain items")] -#[test_case::test_case(true; "annotated items")] +#[test_case::test_case(false, None, false, 5; "plain items")] +#[test_case::test_case(true, None, false, 5; "annotated items")] +#[test_case::test_case(true, Some(100), false, 100; "existing tool budget")] +#[test_case::test_case(false, None, true, 31; "byte policy includes allowance before conversion")] #[tokio::test] -async fn annotated_history_uses_explicit_model_without_a_step(retain_metadata: bool) { +async fn annotated_history_uses_explicit_model_without_a_step( + retain_metadata: bool, + saved_budget: Option, + byte_policy: bool, + expected_budget: usize, +) { let (session, turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx( CodexAuth::from_api_key("Test API Key"), Vec::new(), @@ -2548,8 +2555,11 @@ async fn annotated_history_uses_explicit_model_without_a_step(retain_metadata: b ) .await; let mut model_info = turn_context.model_info().as_ref().clone(); - model_info.truncation_policy = - codex_protocol::openai_models::TruncationPolicyConfig::tokens(/*limit*/ 4); + model_info.truncation_policy = if byte_policy { + codex_protocol::openai_models::TruncationPolicyConfig::bytes(/*limit*/ 101) + } else { + codex_protocol::openai_models::TruncationPolicyConfig::tokens(/*limit*/ 4) + }; let text = "diagnostic line\n".repeat(50); let mut item = ResponseItem::FunctionCallOutput { id: Some(ResponseItemId::with_suffix("fco", "existing")), @@ -2564,24 +2574,52 @@ async fn annotated_history_uses_explicit_model_without_a_step(retain_metadata: b }; item.set_turn_id_if_missing("existing-turn"); item.set_create_time_if_missing(123.into()); - let mut expected = ResponseItemEnvelope { + let expected = ResponseItemEnvelope { item, metadata: retain_metadata.then_some(CodexHarnessMetadata { client_authored: true, + history_truncation_token_limit: saved_budget, ..Default::default() }), }; + let mut expected = vec![ + expected, + ResponseItemEnvelope { + item: ResponseItem::CustomToolCallOutput { + id: Some(ResponseItemId::with_suffix("ctco", "existing")), + call_id: "call-custom".to_string(), + name: None, + output: FunctionCallOutputPayload::from_text(text.clone()), + internal_chat_message_metadata_passthrough: None, + }, + metadata: retain_metadata.then_some(CodexHarnessMetadata { + client_authored: true, + history_truncation_token_limit: saved_budget, + ..Default::default() + }), + }, + ]; + expected[1].item.set_turn_id_if_missing("existing-turn"); + expected[1].item.set_create_time_if_missing(123.into()); session - .record_annotated_conversation_items(&turn_context, &model_info, vec![expected.clone()]) + .record_annotated_conversation_items(&turn_context, &model_info, expected.clone()) .await; - let ResponseItem::FunctionCallOutput { output, .. } = &mut expected.item else { - unreachable!("fixture is a tool output"); - }; - output.body = FunctionCallOutputBody::Text(codex_utils_output_truncation::truncate_text( - &text, - codex_utils_output_truncation::TruncationPolicy::Tokens(4) * 1.2, - )); - assert_eq!(session.clone_history().await.annotated_items(), &[expected]); + for envelope in &mut expected { + envelope + .metadata + .get_or_insert_default() + .history_truncation_token_limit = Some(expected_budget); + let (ResponseItem::FunctionCallOutput { output, .. } + | ResponseItem::CustomToolCallOutput { output, .. }) = &mut envelope.item + else { + unreachable!("fixture is a tool output"); + }; + output.body = FunctionCallOutputBody::Text(codex_utils_output_truncation::truncate_text( + &text, + codex_utils_output_truncation::TruncationPolicy::Tokens(expected_budget), + )); + } + assert_eq!(session.clone_history().await.annotated_items(), &expected); } #[tokio::test] diff --git a/codex-rs/core/src/tools/context.rs b/codex-rs/core/src/tools/context.rs index 72f9481ff3..5e023fd4c0 100644 --- a/codex-rs/core/src/tools/context.rs +++ b/codex-rs/core/src/tools/context.rs @@ -21,6 +21,7 @@ use codex_utils_output_truncation::approx_token_count; use codex_utils_output_truncation::formatted_truncate_text; use codex_utils_output_truncation::truncate_function_output_payload; use codex_utils_output_truncation::truncate_text; +use codex_utils_output_truncation::with_serialization_allowance; use serde::Serialize; use serde_json::Value as JsonValue; use std::num::NonZeroUsize; @@ -124,7 +125,7 @@ impl ToolOutput for McpToolOutput { } fn fallback_token_limit_override(&self) -> Option { - Some((self.truncation_policy * 1.2).token_budget()) + Some(with_serialization_allowance(self.truncation_policy).token_budget()) } fn to_response_item(&self, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem { @@ -173,7 +174,7 @@ impl McpToolOutput { // History receives this budget in tokens. Code Mode keeps the raw result. truncate_function_output_payload( &mut payload, - self.truncation_policy * 1.2, + with_serialization_allowance(self.truncation_policy), estimate_audio_token_count, ); payload @@ -515,7 +516,7 @@ impl ExecCommandToolOutput { fn response_text(&self) -> String { let header = self.response_header(); - let output_budget = (self.truncation_policy * 1.2) + let output_budget = with_serialization_allowance(self.truncation_policy) .byte_budget() .saturating_sub(header.len().saturating_add(/*rhs*/ 1)); let mut policy = self.model_output_policy(); diff --git a/codex-rs/core/src/tools/handlers/mcp_resource.rs b/codex-rs/core/src/tools/handlers/mcp_resource.rs index 5709d05375..d6577f30d6 100644 --- a/codex-rs/core/src/tools/handlers/mcp_resource.rs +++ b/codex-rs/core/src/tools/handlers/mcp_resource.rs @@ -13,6 +13,7 @@ use codex_protocol::mcp::CallToolResult; use codex_protocol::models::function_call_output_content_items_to_text; use codex_protocol::protocol::TruncationPolicy; use codex_utils_output_truncation::truncate_text; +use codex_utils_output_truncation::with_serialization_allowance; use rmcp::model::ListResourceTemplatesResult; use rmcp::model::ListResourcesResult; use rmcp::model::PaginatedRequestParams; @@ -362,7 +363,7 @@ where })?; // Match regular MCP tool outputs by bounding the copy persisted to the // rollout and injected into model context. - let content = truncate_text(&content, truncation_policy * 1.2); + let content = truncate_text(&content, with_serialization_allowance(truncation_policy)); Ok(FunctionToolOutput::from_text(content, Some(true))) } diff --git a/codex-rs/core/src/tools/registry.rs b/codex-rs/core/src/tools/registry.rs index fb2ccbb36e..4e7d5a1a89 100644 --- a/codex-rs/core/src/tools/registry.rs +++ b/codex-rs/core/src/tools/registry.rs @@ -203,7 +203,7 @@ impl AnyToolResult { metadata: result .fallback_token_limit_override() .map(|limit| CodexHarnessMetadata { - fallback_token_limit_override: Some(limit), + history_truncation_token_limit: Some(limit), ..Default::default() }), } diff --git a/codex-rs/core/src/tools/registry_tests.rs b/codex-rs/core/src/tools/registry_tests.rs index 9808432f86..edc68df33b 100644 --- a/codex-rs/core/src/tools/registry_tests.rs +++ b/codex-rs/core/src/tools/registry_tests.rs @@ -629,7 +629,7 @@ fn post_tool_use_feedback_output_preserves_fallback_token_limit_override( output: FunctionCallOutputPayload::from_text("hook feedback".to_string()), }), metadata: Some(CodexHarnessMetadata { - fallback_token_limit_override: Some(expected_token_limit), + history_truncation_token_limit: Some(expected_token_limit), ..Default::default() }), } diff --git a/codex-rs/core/tests/suite/step_settings.rs b/codex-rs/core/tests/suite/step_settings.rs index 998b4173ba..76c6083673 100644 --- a/codex-rs/core/tests/suite/step_settings.rs +++ b/codex-rs/core/tests/suite/step_settings.rs @@ -3,6 +3,7 @@ use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use codex_config::McpServerConfig; use codex_core::CodexThread; +use codex_core::ForkSnapshot; use codex_core::StartThreadOptions; use codex_core::TurnInputRequest; use codex_core::config::Config; @@ -29,6 +30,7 @@ use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem; use codex_protocol::dynamic_tools::DynamicToolFunctionSpec; use codex_protocol::dynamic_tools::DynamicToolResponse; use codex_protocol::dynamic_tools::DynamicToolSpec; +use codex_protocol::mcp::ClientMcpExtensions; use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ApplyPatchToolType; @@ -83,6 +85,7 @@ use core_test_support::responses::ev_function_call_with_namespace; use core_test_support::responses::ev_response_created; use core_test_support::responses::mount_models_once; use core_test_support::responses::mount_response_sequence; +use core_test_support::responses::mount_sse_once; use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::sse; use core_test_support::responses::sse_completed; @@ -286,7 +289,7 @@ fn request_turn_id(request: &ResponsesRequest) -> String { // Dynamic tools return the original payload, so handler truncation cannot hide a recorder bug. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn tool_result_history_keeps_originating_model_across_switch() -> Result<()> { +async fn tool_result_history_keeps_originating_model_across_switch_and_replay() -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; let mut test = step_settings_test() @@ -438,10 +441,8 @@ async fn tool_result_history_keeps_originating_model_across_switch() -> Result<( // Persistence and raw notifications retain the prepared, untruncated payload in append order. test.codex.shutdown_and_wait().await?; - let history = codex_rollout::RolloutRecorder::get_rollout_history( - &test.codex.rollout_path().expect("rollout path"), - ) - .await?; + let rollout_path = test.codex.rollout_path().expect("rollout path"); + let history = codex_rollout::RolloutRecorder::get_rollout_history(&rollout_path).await?; let saved_outputs = history .get_rollout_items() .iter() @@ -449,12 +450,183 @@ async fn tool_result_history_keeps_originating_model_across_switch() -> Result<( RolloutItem::ResponseItem(envelope) if matches!(&envelope.item, ResponseItem::FunctionCallOutput { .. }) => { - Some(serde_json::to_value(&envelope.item).expect("saved output")) + Some(( + serde_json::to_value(&envelope.item).expect("saved output"), + envelope + .metadata + .as_ref() + .and_then(|metadata| metadata.history_truncation_token_limit), + )) } _ => None, }) .collect::>(); - assert_eq!(saved_outputs, raw_outputs); + assert_eq!( + saved_outputs, + raw_outputs + .into_iter() + .zip([Some(120), Some(480)]) + .collect::>() + ); + + // Resume and fork under both limits. A's output must not grow under B, and + // B's output must not shrink under A; image preparation and item IDs also survive. + for model in [MODEL_A, MODEL_B] { + let mut replay_config = test.config.clone(); + replay_config.model = Some(model.to_string()); + let resumed = test + .thread_manager + .resume_thread_from_rollout( + replay_config.clone(), + rollout_path.clone(), + codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("dummy")), + /*parent_trace*/ None, + ClientMcpExtensions::default(), + ) + .await? + .thread; + let forked = test + .thread_manager + .fork_thread( + ForkSnapshot::Interrupted, + StartThreadOptions::new(replay_config), + rollout_path.clone(), + ) + .await? + .thread; + for thread in [resumed, forked] { + let replay = mount_sse_once(&server, sse_completed("resp-replay")).await; + thread + .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { + text: "review saved diagnostics".to_string(), + text_elements: Vec::new(), + }])) + .await?; + wait_for_event(&thread, |event| matches!(event, EventMsg::TurnComplete(_))).await; + let request = replay.single_request(); + assert_eq!(request.body_json()["model"], model); + for call_id in ["call-a", "call-b"] { + assert_eq!( + request.function_call_output(call_id), + requests[2].function_call_output(call_id) + ); + } + thread.shutdown_and_wait().await?; + } + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn custom_tool_output_replay_preserves_originating_budget() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + let test = step_settings_test() + .with_model(MODEL_B) + .with_config(|config| { + config.features.enable(Feature::CodeMode).unwrap(); + for model in &mut config.model_catalog.as_mut().expect("models").models { + model.truncation_policy = + TruncationPolicyConfig::tokens(if model.slug == MODEL_B { 400 } else { 100 }); + model.tool_mode = Some(ToolMode::CodeModeOnly); + model.use_responses_lite = false; + } + }) + .build_with_auto_env(&server) + .await?; + let responses = mount_sse_sequence( + &server, + vec![ + sse(vec![ + ev_response_created("resp-custom"), + ev_custom_tool_call( + "call-custom", + "exec", + "text('diagnostic line\\n'.repeat(500));", + ), + ev_completed("resp-custom"), + ]), + sse_completed("resp-done"), + ], + ) + .await; + test.submit_text_turn("collect diagnostics").await?; + let live_output = responses.requests()[1].custom_tool_call_output("call-custom"); + let text = "diagnostic line\n".repeat(500); + let live_text = live_output["output"][1]["text"] + .as_str() + .expect("bounded custom output"); + assert!(live_text.len() < text.len()); + // A's smaller history budget would truncate this result again without the saved budget. + assert_ne!( + truncate_text(live_text, TruncationPolicy::Tokens(120)), + live_text + ); + + test.codex.shutdown_and_wait().await?; + let rollout_path = test.codex.rollout_path().expect("rollout path"); + let history = codex_rollout::RolloutRecorder::get_rollout_history(&rollout_path).await?; + let saved = history + .get_rollout_items() + .iter() + .find_map(|item| match item { + RolloutItem::ResponseItem(envelope) + if matches!(&envelope.item, ResponseItem::CustomToolCallOutput { call_id, .. } if call_id == "call-custom") => + { + Some(envelope) + } + _ => None, + }) + .expect("saved custom output"); + assert_eq!( + saved + .metadata + .as_ref() + .and_then(|metadata| metadata.history_truncation_token_limit), + Some(480) + ); + assert_eq!( + serde_json::to_value(&saved.item)?["output"][1]["text"], + text + ); + + let mut replay_config = test.config.clone(); + replay_config.model = Some(MODEL_A.to_string()); + let resumed = test + .thread_manager + .resume_thread_from_rollout( + replay_config.clone(), + rollout_path.clone(), + codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("dummy")), + /*parent_trace*/ None, + ClientMcpExtensions::default(), + ) + .await? + .thread; + let forked = test + .thread_manager + .fork_thread( + ForkSnapshot::Interrupted, + StartThreadOptions::new(replay_config), + rollout_path, + ) + .await? + .thread; + for thread in [resumed, forked] { + let replay = mount_sse_once(&server, sse_completed("resp-replay")).await; + thread + .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { + text: "review saved diagnostics".to_string(), + text_elements: Vec::new(), + }])) + .await?; + wait_for_event(&thread, |event| matches!(event, EventMsg::TurnComplete(_))).await; + let request = replay.single_request(); + assert_eq!(request.body_json()["model"], MODEL_A); + assert_eq!(request.custom_tool_call_output("call-custom"), live_output); + thread.shutdown_and_wait().await?; + } Ok(()) } diff --git a/codex-rs/history/src/lib.rs b/codex-rs/history/src/lib.rs index 258c32291f..77e1b66277 100644 --- a/codex-rs/history/src/lib.rs +++ b/codex-rs/history/src/lib.rs @@ -50,10 +50,14 @@ pub struct CodexHarnessMetadata { #[serde(default)] pub client_authored: bool, - /// Overrides history's fallback truncation budget, including on resume. - /// Measured in tokens, with any tool-specific allowance already included. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub fallback_token_limit_override: Option, + /// The originating history budget, including any tool-specific allowance. + /// Measured in tokens and reused when replaying persisted history. + #[serde( + default, + rename = "fallback_token_limit_override", + skip_serializing_if = "Option::is_none" + )] + pub history_truncation_token_limit: Option, /// Whether a response configuration update was created by the Codex harness itself. #[serde(default, skip_serializing_if = "std::ops::Not::not")] diff --git a/codex-rs/history/src/tests.rs b/codex-rs/history/src/tests.rs index c9a6597201..b4d922907a 100644 --- a/codex-rs/history/src/tests.rs +++ b/codex-rs/history/src/tests.rs @@ -78,7 +78,7 @@ fn response_item_envelope_stores_metadata_beside_rollout_payload() -> Result<()> item: response_item.clone(), metadata: Some(CodexHarnessMetadata { client_authored: true, - fallback_token_limit_override: Some(20_000), + history_truncation_token_limit: Some(20_000), inherited_user_message: true, ..Default::default() }), @@ -110,7 +110,7 @@ fn response_item_envelope_stores_metadata_beside_rollout_payload() -> Result<()> envelope.metadata, Some(CodexHarnessMetadata { client_authored: true, - fallback_token_limit_override: Some(20_000), + history_truncation_token_limit: Some(20_000), inherited_user_message: true, ..Default::default() }) diff --git a/codex-rs/tools/src/tool_call.rs b/codex-rs/tools/src/tool_call.rs index 3ba56a9a4e..65336a789f 100644 --- a/codex-rs/tools/src/tool_call.rs +++ b/codex-rs/tools/src/tool_call.rs @@ -8,6 +8,7 @@ use codex_protocol::models::ResponseItem; use codex_protocol::protocol::EventMsg; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_output_truncation::TruncationPolicy; +use codex_utils_output_truncation::with_serialization_allowance; use std::future::Future; use std::marker::PhantomData; use std::pin::Pin; @@ -147,9 +148,8 @@ impl ToolCall<'_> { /// Callers must include serialization overhead when fitting a response to this budget. pub fn response_byte_budget(&self, max_response_bytes: usize) -> usize { match &self.source { - ToolCallSource::Direct => { - max_response_bytes.min((self.truncation_policy * 1.2).byte_budget()) - } + ToolCallSource::Direct => max_response_bytes + .min(with_serialization_allowance(self.truncation_policy).byte_budget()), ToolCallSource::CodeMode { cell_id: _, runtime_tool_call_id: _, diff --git a/codex-rs/utils/output-truncation/src/lib.rs b/codex-rs/utils/output-truncation/src/lib.rs index e612457396..f4f502ab28 100644 --- a/codex-rs/utils/output-truncation/src/lib.rs +++ b/codex-rs/utils/output-truncation/src/lib.rs @@ -11,6 +11,12 @@ use codex_utils_string::truncate_middle_with_token_budget; pub use codex_protocol::protocol::TruncationPolicy; +/// Adds the existing 20% allowance for serialization and headers. +/// Saved history budgets already include this allowance. +pub fn with_serialization_allowance(policy: TruncationPolicy) -> TruncationPolicy { + policy * 1.2 +} + pub fn formatted_truncate_text(content: &str, policy: TruncationPolicy) -> String { if content.len() <= policy.byte_budget() { return content.to_string();