mirror of
https://github.com/openai/codex.git
synced 2026-10-28 00:03:58 +00:00
Keep unified exec output collection bounded (#32150)
## Why Unified exec can drain process output multiple times while waiting for a command. Collecting those drains into an uncapped buffer allows large commands to exceed the output collection limit. ## What changed - Accumulate drained output through the capped head/tail buffer so collection remains bounded while preserving the beginning and end of the stream. - Include a `... N bytes omitted ...` marker when the collection cap drops output. - Calculate the original token estimate from all observed bytes and preserve omission metadata through truncation and sandbox-denial responses. ## Testing - Cover repeated drains, previously omitted output, omission markers, and large-output summaries. GitOrigin-RevId: c6e2efeb875ed8f35914365eab7431f25fd3484c
This commit is contained in:
@@ -7,6 +7,7 @@ use crate::tools::TELEMETRY_PREVIEW_MAX_BYTES;
|
||||
use crate::tools::TELEMETRY_PREVIEW_MAX_LINES;
|
||||
use crate::tools::TELEMETRY_PREVIEW_TRUNCATION_NOTICE;
|
||||
use crate::turn_diff_tracker::TurnDiffTracker;
|
||||
use crate::unified_exec::format_output_omission_marker;
|
||||
use crate::unified_exec::resolve_max_tokens;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
@@ -17,10 +18,13 @@ use codex_protocol::models::function_call_output_content_items_to_text;
|
||||
use codex_tools::LoadableToolSpec;
|
||||
use codex_tools::ToolName;
|
||||
use codex_utils_output_truncation::TruncationPolicy;
|
||||
use codex_utils_output_truncation::approx_token_count;
|
||||
use codex_utils_output_truncation::formatted_truncate_text;
|
||||
use codex_utils_output_truncation::truncate_text;
|
||||
use codex_utils_string::take_bytes_at_char_boundary;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -319,6 +323,8 @@ pub struct ExecCommandToolOutput {
|
||||
pub process_id: Option<i32>,
|
||||
pub exit_code: Option<i32>,
|
||||
pub original_token_count: Option<usize>,
|
||||
/// Bytes omitted by the output collection cap before model-facing truncation.
|
||||
pub output_omitted_bytes: Option<NonZeroUsize>,
|
||||
pub hook_command: Option<String>,
|
||||
}
|
||||
|
||||
@@ -406,7 +412,32 @@ impl ExecCommandToolOutput {
|
||||
|
||||
pub(crate) fn truncated_output(&self, max_tokens: usize) -> String {
|
||||
let text = String::from_utf8_lossy(&self.raw_output).to_string();
|
||||
formatted_truncate_text(&text, TruncationPolicy::Tokens(max_tokens))
|
||||
let policy = TruncationPolicy::Tokens(max_tokens);
|
||||
let Some(omitted_bytes) = self.output_omitted_bytes else {
|
||||
return formatted_truncate_text(&text, policy);
|
||||
};
|
||||
|
||||
let marker = format_output_omission_marker(omitted_bytes.get());
|
||||
if text.len() <= policy.byte_budget() {
|
||||
return if text.contains(&marker) {
|
||||
text
|
||||
} else {
|
||||
format!("{marker}\n{text}")
|
||||
};
|
||||
}
|
||||
|
||||
let original_token_count = self
|
||||
.original_token_count
|
||||
.unwrap_or_else(|| approx_token_count(&text));
|
||||
let truncated = truncate_text(&text, policy);
|
||||
let omission_notice = if truncated.contains(&marker) {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{marker}\n")
|
||||
};
|
||||
format!(
|
||||
"Warning: truncated output (original token count: {original_token_count})\n{omission_notice}\n{truncated}"
|
||||
)
|
||||
}
|
||||
|
||||
fn response_text(&self) -> String {
|
||||
|
||||
@@ -434,6 +434,7 @@ fn exec_command_tool_output_formats_truncated_response() {
|
||||
process_id: None,
|
||||
exit_code: Some(0),
|
||||
original_token_count: Some(10),
|
||||
output_omitted_bytes: None,
|
||||
hook_command: None,
|
||||
}
|
||||
.to_response_item("call-42", &payload);
|
||||
@@ -461,3 +462,42 @@ fn exec_command_tool_output_formats_truncated_response() {
|
||||
other => panic!("expected FunctionCallOutput, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_command_tool_output_preserves_omission_metadata_when_truncated() {
|
||||
let payload = ToolPayload::Function {
|
||||
arguments: "{}".to_string(),
|
||||
};
|
||||
let marker = format_output_omission_marker(/*omitted_bytes*/ 123_456);
|
||||
let raw_output = format!(
|
||||
"HEAD-{}\n{marker}\nTAIL-{}",
|
||||
"a".repeat(/*n*/ 100),
|
||||
"z".repeat(/*n*/ 100)
|
||||
)
|
||||
.into_bytes();
|
||||
let response = ExecCommandToolOutput {
|
||||
event_call_id: "call-omitted".to_string(),
|
||||
chunk_id: "abc123".to_string(),
|
||||
wall_time: std::time::Duration::from_millis(/*millis*/ 1250),
|
||||
raw_output,
|
||||
truncation_policy: TruncationPolicy::Tokens(10_000),
|
||||
max_output_tokens: Some(4),
|
||||
process_id: None,
|
||||
exit_code: Some(0),
|
||||
original_token_count: Some(42_000),
|
||||
output_omitted_bytes: NonZeroUsize::new(/*n*/ 123_456),
|
||||
hook_command: None,
|
||||
}
|
||||
.to_response_item("call-omitted", &payload);
|
||||
|
||||
let ResponseInputItem::FunctionCallOutput { output, .. } = response else {
|
||||
panic!("expected FunctionCallOutput");
|
||||
};
|
||||
let text = output
|
||||
.body
|
||||
.to_text()
|
||||
.expect("exec output should serialize as text");
|
||||
assert!(text.contains("Original token count: 42000"));
|
||||
assert!(text.contains("Warning: truncated output (original token count: 42000)"));
|
||||
assert_eq!(text.matches(&marker).count(), 1);
|
||||
}
|
||||
|
||||
@@ -335,6 +335,7 @@ impl ExecCommandHandler {
|
||||
process_id: None,
|
||||
exit_code: None,
|
||||
original_token_count: None,
|
||||
output_omitted_bytes: None,
|
||||
hook_command: None,
|
||||
}));
|
||||
}
|
||||
@@ -367,9 +368,15 @@ impl ExecCommandHandler {
|
||||
.await
|
||||
{
|
||||
Ok(response) => Ok(boxed_tool_output(response)),
|
||||
Err(UnifiedExecError::SandboxDenied { output, .. }) => {
|
||||
Err(UnifiedExecError::SandboxDenied {
|
||||
output,
|
||||
original_token_count,
|
||||
output_omitted_bytes,
|
||||
..
|
||||
}) => {
|
||||
let output_text = output.aggregated_output.text;
|
||||
let original_token_count = approx_token_count(&output_text);
|
||||
let original_token_count =
|
||||
original_token_count.unwrap_or_else(|| approx_token_count(&output_text));
|
||||
Ok(boxed_tool_output(ExecCommandToolOutput {
|
||||
event_call_id: context.call_id.clone(),
|
||||
chunk_id: generate_chunk_id(),
|
||||
@@ -382,6 +389,7 @@ impl ExecCommandHandler {
|
||||
process_id: None,
|
||||
exit_code: Some(output.exit_code),
|
||||
original_token_count: Some(original_token_count),
|
||||
output_omitted_bytes,
|
||||
hook_command: Some(hook_command),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -301,6 +301,7 @@ async fn exec_command_post_tool_use_payload_uses_output_for_noninteractive_one_s
|
||||
process_id: None,
|
||||
exit_code: Some(0),
|
||||
original_token_count: None,
|
||||
output_omitted_bytes: None,
|
||||
hook_command: Some("echo three".to_string()),
|
||||
};
|
||||
let invocation = invocation_for_payload("exec_command", "call-43", payload).await;
|
||||
@@ -331,6 +332,7 @@ async fn exec_command_post_tool_use_payload_uses_output_for_interactive_completi
|
||||
process_id: None,
|
||||
exit_code: Some(0),
|
||||
original_token_count: None,
|
||||
output_omitted_bytes: None,
|
||||
hook_command: Some("echo three".to_string()),
|
||||
};
|
||||
let invocation = invocation_for_payload("exec_command", "call-44", payload).await;
|
||||
@@ -362,6 +364,7 @@ async fn exec_command_post_tool_use_payload_skips_running_sessions() {
|
||||
process_id: Some(45),
|
||||
exit_code: None,
|
||||
original_token_count: None,
|
||||
output_omitted_bytes: None,
|
||||
hook_command: Some("echo three".to_string()),
|
||||
};
|
||||
let invocation = invocation_for_payload("exec_command", "call-45", payload).await;
|
||||
@@ -388,6 +391,7 @@ async fn write_stdin_post_tool_use_payload_uses_original_exec_call_id_and_comman
|
||||
process_id: None,
|
||||
exit_code: Some(0),
|
||||
original_token_count: None,
|
||||
output_omitted_bytes: None,
|
||||
hook_command: Some("sleep 1; echo finished".to_string()),
|
||||
};
|
||||
let invocation = invocation_for_payload("write_stdin", "write-stdin-call", payload).await;
|
||||
@@ -419,6 +423,7 @@ async fn write_stdin_post_tool_use_payload_keeps_parallel_session_metadata_separ
|
||||
process_id: None,
|
||||
exit_code: Some(0),
|
||||
original_token_count: None,
|
||||
output_omitted_bytes: None,
|
||||
hook_command: Some("sleep 2; echo alpha".to_string()),
|
||||
};
|
||||
let output_b = ExecCommandToolOutput {
|
||||
@@ -431,6 +436,7 @@ async fn write_stdin_post_tool_use_payload_keeps_parallel_session_metadata_separ
|
||||
process_id: None,
|
||||
exit_code: Some(0),
|
||||
original_token_count: None,
|
||||
output_omitted_bytes: None,
|
||||
hook_command: Some("sleep 1; echo beta".to_string()),
|
||||
};
|
||||
let invocation_b = invocation_for_payload("write_stdin", "write-call-b", payload.clone()).await;
|
||||
|
||||
Reference in New Issue
Block a user