Enable gRPC and host output truncation by default for Code Mode (#59973)

## What changed

Mark `code_mode_host_grpc` and `code_mode_host_output_truncation` as stable and enable them by default. Code Mode now defaults to gRPC over stdio and truncates output in the host before sending it to Core.

## Testing

Update integration tests to exercise the default configuration and explicit feature opt-outs, covering output budgets, truncation notices, and wait termination. Keep the stdio reconnection test explicitly configured for that transport.

GitOrigin-RevId: 5847a249d698fb9b1ecf4921aca4e6efc501f890
This commit is contained in:
Sean Huang
2026-11-06 20:29:09 +00:00
committed by copyberry
parent 8842c09a7f
commit 247934cfe5
3 changed files with 61 additions and 116 deletions
+55 -110
View File
@@ -12,11 +12,6 @@ use anyhow::Result;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use codex_analytics::AnalyticsEventsClient;
use codex_code_mode::CodeModeOutputTruncation;
use codex_code_mode::CodeModeSessionOptions;
use codex_code_mode::CodeModeSessionProvider;
use codex_code_mode::CodeModeSessionProviderFuture;
use codex_code_mode::ProcessOwnedCodeModeSessionProvider;
use codex_config::types::McpServerConfig;
use codex_config::types::McpServerTransportConfig;
use codex_core::StartThreadOptions;
@@ -25,7 +20,6 @@ use codex_core::config::Config;
use codex_core::config::Constrained;
use codex_core::config::CurrentTimeReminderConfig;
use codex_core::context::NodeReplReviewEvidence;
use codex_core::test_support::ProcessOwnedGrpcCodeModeSessionProvider;
use codex_extension_api::ExtensionData;
use codex_extension_api::ExtensionFuture;
use codex_extension_api::ExtensionRegistryBuilder;
@@ -315,72 +309,15 @@ async fn run_code_mode_turn_with_builder(
Ok((test, second_mock))
}
// Request server truncation from the real host and verify what it negotiated.
struct ServerOutputProvider(Arc<dyn CodeModeSessionProvider>);
impl CodeModeSessionProvider for ServerOutputProvider {
fn availability(&self) -> std::result::Result<(), String> {
self.0.availability()
}
fn create_session(&self) -> CodeModeSessionProviderFuture<'_> {
Box::pin(async {
let session = self
.0
.create_session_with_options(CodeModeSessionOptions {
output_truncation: CodeModeOutputTruncation::Server,
..CodeModeSessionOptions::default()
})
.await?;
assert_eq!(
session.output_truncation(),
CodeModeOutputTruncation::Server
);
Ok(session)
fn code_mode_test_builder(disabled_features: &'static [Feature]) -> TestCodexBuilder {
test_codex()
.with_model("test-gpt-5.1-codex")
.with_config(move |config| {
config.features.enable(Feature::CodeMode).unwrap();
for &feature in disabled_features {
config.features.disable(feature).unwrap();
}
})
}
}
pub(super) enum OutputHost {
DefaultClient,
ServerStdio,
ServerGrpc,
}
impl OutputHost {
pub(super) fn test_codex(self) -> TestCodexBuilder {
let builder = test_codex()
.with_model("test-gpt-5.1-codex")
.with_config(|config| {
config
.features
.enable(Feature::CodeMode)
.expect("enable code mode");
});
let host = || {
codex_utils_cargo_bin::cargo_bin("codex-code-mode-host")
.expect("locate the code-mode host")
};
let provider: Arc<dyn CodeModeSessionProvider> = match self {
Self::DefaultClient => return builder,
Self::ServerStdio => Arc::new(ProcessOwnedCodeModeSessionProvider::with_host_program(
host(),
)),
Self::ServerGrpc => Arc::new(
ProcessOwnedGrpcCodeModeSessionProvider::with_host_program(host()),
),
};
builder
.with_config(|config| {
config
.features
.disable(Feature::CodeModeHost)
.expect("use the injected provider");
})
.with_thread_manager(move |manager| {
manager.with_code_mode_session_provider(Arc::new(ServerOutputProvider(provider)))
})
}
}
async fn run_unavailable_code_mode_turn(
@@ -4028,7 +3965,7 @@ text(JSON.stringify({{
#[cfg_attr(windows, ignore = "no exec_command on Windows")]
#[test_case(false; "legacy_stdio")]
#[test_case(true; "grpc_stdio")]
#[test_case(true; "default_grpc_stdio")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn code_mode_only_can_call_nested_tools(grpc_stdio: bool) -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -4070,8 +4007,8 @@ text(output.output);
let mut builder = test_codex().with_config(move |config| {
let _ = config.features.enable(Feature::CodeModeOnly);
if grpc_stdio {
let _ = config.features.enable(Feature::CodeModeHostGrpc);
if !grpc_stdio {
config.features.disable(Feature::CodeModeHostGrpc).unwrap();
}
});
#[cfg(unix)]
@@ -4644,11 +4581,13 @@ text(`Variable truncated: ${resultVariableWasTruncated ? "True" : "False"}. Vari
// The outer directive limits output after JavaScript emits it; it does not
// limit `result.output` returned by the nested command.
#[cfg_attr(windows, ignore = "no exec_command on Windows")]
#[test_case(OutputHost::DefaultClient; "client_stdio")]
#[test_case(OutputHost::ServerStdio; "server_stdio")]
#[test_case(OutputHost::ServerGrpc; "server_grpc")]
#[test_case(&[]; "default_grpc")]
#[test_case(&[Feature::CodeModeHostGrpc]; "server_stdio")]
#[test_case(&[Feature::CodeModeHostGrpc, Feature::CodeModeHostOutputTruncation]; "client_stdio")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn code_mode_exec_outer_limit_truncates_emitted_output(host: OutputHost) -> Result<()> {
async fn code_mode_exec_outer_limit_truncates_emitted_output(
disabled_features: &'static [Feature],
) -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
@@ -4661,7 +4600,7 @@ const result = await tools.exec_command({
});
text(result.output);
"#,
host.test_codex(),
code_mode_test_builder(disabled_features),
)
.await?;
@@ -4720,11 +4659,12 @@ Error:\ boom\n
// An overflow notice is outside the observation budget. This distinguishes
// actual server formatting from Core silently repeating client truncation.
#[test_case(false; "stdio")]
#[test_case(true; "grpc")]
#[test_case(None; "default")]
#[test_case(Some(Feature::CodeModeHostGrpc); "server_stdio")]
#[test_case(Some(Feature::CodeModeHostOutputTruncation); "client_grpc")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn code_mode_collector_notice_survives_the_observation_budget(
grpc_stdio: bool,
async fn code_mode_zero_budget_reports_host_truncation(
disabled_feature: Option<Feature>,
) -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -4738,14 +4678,8 @@ text("x".repeat(2 * 1024 * 1024));
text("end");
"#,
move |config| {
let _ = config.features.enable(Feature::CodeModeHost);
let _ = config
.features
.enable(Feature::CodeModeHostOutputTruncation);
if grpc_stdio {
let _ = config.features.enable(Feature::CodeModeHostGrpc);
} else {
let _ = config.features.disable(Feature::CodeModeHostGrpc);
if let Some(feature) = disabled_feature {
config.features.disable(feature).unwrap();
}
},
)
@@ -4753,11 +4687,18 @@ text("end");
let (output, success) =
custom_tool_output_body_and_success(&follow_up.single_request(), "call-1");
assert_ne!(success, Some(false), "{output}");
assert!(
output.contains("Code Mode session output limit reached")
&& output.contains("text segments, 0 images, 0 audio items"),
"host omission notice must survive Core's zero-token exec budget: {output}"
);
if disabled_feature == Some(Feature::CodeModeHostOutputTruncation) {
assert!(
!output.contains("Code Mode session output limit reached"),
"{output}"
);
} else {
assert!(
output.contains("Code Mode session output limit reached")
&& output.contains("text segments, 0 images, 0 audio items"),
"host omission notice must survive Core's zero-token exec budget: {output}"
);
}
Ok(())
}
@@ -4810,15 +4751,16 @@ try {
Ok(())
}
/// A stalled host wait must return its timeout to the model and reconnect for the next exec.
/// The framed stdio host reconnects on the first exec after reporting a wait timeout.
#[tokio::test(flavor = "current_thread")]
async fn code_mode_wait_timeout_reconnects_on_next_exec() -> Result<()> {
async fn code_mode_stdio_wait_timeout_reconnects_on_next_exec() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let mut builder = test_codex()
.with_model("test-gpt-5.1-codex")
.with_config(|config| {
config.features.disable(Feature::CodeModeHostGrpc).unwrap();
config
.features
.enable(Feature::CodeMode)
@@ -5688,19 +5630,20 @@ impl ToolLifecycleContributor for TerminationOutputReady {
}
#[cfg_attr(windows, ignore = "no exec_command on Windows")]
#[test_case(OutputHost::DefaultClient; "client_stdio")]
#[test_case(OutputHost::ServerStdio; "server_stdio")]
#[test_case(OutputHost::ServerGrpc; "server_grpc")]
#[test_case(&[]; "default_grpc")]
#[test_case(&[Feature::CodeModeHostGrpc]; "server_stdio")]
#[test_case(&[Feature::CodeModeHostGrpc, Feature::CodeModeHostOutputTruncation]; "client_stdio")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn code_mode_wait_can_terminate_and_continue(host: OutputHost) -> Result<()> {
async fn code_mode_wait_can_terminate_and_continue(
disabled_features: &'static [Feature],
) -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let mut extensions = ExtensionRegistryBuilder::<Config>::new();
extensions.tool_lifecycle_contributor(Arc::new(TerminationOutputReady::default()));
let mut builder = host
.test_codex()
.with_extensions(Arc::new(extensions.build()));
let mut builder =
code_mode_test_builder(disabled_features).with_extensions(Arc::new(extensions.build()));
let test = builder.build(&server).await?;
let termination_gate = test.workspace_path("code-mode-terminate.ready");
let termination_wait = wait_for_file_source(&termination_gate)?;
@@ -6418,15 +6361,17 @@ async fn code_mode_interrupt_terminates_active_cells_and_nested_tools() -> Resul
Ok(())
}
#[test_case(OutputHost::DefaultClient; "client_stdio")]
#[test_case(OutputHost::ServerStdio; "server_stdio")]
#[test_case(OutputHost::ServerGrpc; "server_grpc")]
#[test_case(&[]; "default_grpc")]
#[test_case(&[Feature::CodeModeHostGrpc]; "server_stdio")]
#[test_case(&[Feature::CodeModeHostGrpc, Feature::CodeModeHostOutputTruncation]; "client_stdio")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn code_mode_wait_uses_its_own_max_tokens_budget(host: OutputHost) -> Result<()> {
async fn code_mode_wait_uses_its_own_max_tokens_budget(
disabled_features: &'static [Feature],
) -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let mut builder = host.test_codex();
let mut builder = code_mode_test_builder(disabled_features);
let test = builder.build_with_auto_env(&server).await?;
// Both transports accept exec budgets above i32::MAX. The wait uses its own budget.
+2 -2
View File
@@ -2355,11 +2355,11 @@ text(`MCP: ${ping.structuredContent?.echo ?? "missing"}`);"#,
async fn astra_reads_code_mode_call_timing() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let test = super::code_mode::OutputHost::ServerGrpc
.test_codex()
let test = test_codex()
.with_model("gpt-6-astra")
.with_config(|config| {
configure_scenario_catalog(config);
config.features.enable(Feature::CodeMode).unwrap();
// Use the selected cwd as the workspace root on local and remote executors.
config.workspace_roots = vec![config.cwd.clone()];
config.code_mode.experimental_show_cell_overhead = true;
+4 -4
View File
@@ -1321,14 +1321,14 @@ pub const FEATURES: &[FeatureSpec] = &[
FeatureSpec {
id: Feature::CodeModeHostGrpc,
key: "code_mode_host_grpc",
stage: Stage::UnderDevelopment,
default_enabled: false,
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::CodeModeHostOutputTruncation,
key: "code_mode_host_output_truncation",
stage: Stage::UnderDevelopment,
default_enabled: false,
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::CodeModePrewarm,