Support negotiated server-side truncation in Code Mode clients (#58125)

## Why

Host-side truncation can bound Code Mode output before it crosses the transport, including script errors that would otherwise exceed the frame limit.

## What changed

- Negotiate server-side output truncation for stdio and gRPC sessions, with client-side fallback for older hosts.
- Forward output budgets for execution, waiting, and termination when server-side truncation is selected. Avoid truncating host-formatted output or appending script errors a second time.
- Preserve the negotiated mode across reconnects and reject hosts that cannot maintain it.

## Testing

Add transport and integration coverage for output budgets, oversized script errors, reconnect compatibility, and preservation of output-limit notices with a zero-token budget.

GitOrigin-RevId: 2075101e4845eca64f45491c27a3b326788645c8
This commit is contained in:
Sean Huang
2026-10-29 16:57:12 +00:00
committed by copyberry
parent 3062f7aa4a
commit 09db085f94
21 changed files with 751 additions and 154 deletions
+50 -15
View File
@@ -2,13 +2,16 @@ use std::io;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex as StdMutex;
use std::sync::OnceLock;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use codex_code_mode_protocol::CodeModeOutputTruncation;
use codex_code_mode_protocol::CodeModeSession;
use codex_code_mode_protocol::CodeModeSessionCellExecutionLimits;
use codex_code_mode_protocol::CodeModeSessionDelegate;
use codex_code_mode_protocol::CodeModeSessionOptions;
use codex_code_mode_protocol::CodeModeSessionProvider;
use codex_code_mode_protocol::CodeModeSessionProviderFuture;
use codex_code_mode_protocol::CodeModeSessionResultFuture;
@@ -83,7 +86,17 @@ impl CodeModeSessionProvider for ProcessOwnedCodeModeSessionProvider {
&'a self,
limits: CodeModeSessionCellExecutionLimits,
) -> CodeModeSessionProviderFuture<'a> {
Box::pin(create_host_session(self.process_host(), limits))
self.create_session_with_options(CodeModeSessionOptions {
cell_execution_limits: limits,
..CodeModeSessionOptions::default()
})
}
fn create_session_with_options<'a>(
&'a self,
options: CodeModeSessionOptions,
) -> CodeModeSessionProviderFuture<'a> {
Box::pin(create_host_session(self.process_host(), options))
}
}
@@ -106,9 +119,9 @@ impl CodeModeSessionProvider for DisabledCodeModeSessionProvider {
async fn create_host_session(
host: Arc<OwnedCodeModeHost>,
limits: CodeModeSessionCellExecutionLimits,
options: CodeModeSessionOptions,
) -> Result<Arc<dyn CodeModeSession>, String> {
let session = ProcessOwnedCodeModeSession::with_host(host, limits);
let session = ProcessOwnedCodeModeSession::with_host(host, options);
session.connection().await?;
Ok(Arc::new(session))
}
@@ -218,7 +231,9 @@ struct SessionBinding {
struct SessionInner {
host: Arc<OwnedCodeModeHost>,
limits: CodeModeSessionCellExecutionLimits,
options: CodeModeSessionOptions,
// The first successful open fixes the output policy for all generations of this session.
output_truncation: OnceLock<CodeModeOutputTruncation>,
state: StdMutex<SessionState>,
shutdown_requested: AtomicBool,
shutdown_result: StdMutex<Option<ShutdownResultReceiver>>,
@@ -236,15 +251,16 @@ impl ProcessOwnedCodeModeSession {
Arc::new(OwnedCodeModeHost::new(
InstallContext::current().code_mode_host_program(),
)),
CodeModeSessionCellExecutionLimits::default(),
CodeModeSessionOptions::default(),
)
}
fn with_host(host: Arc<OwnedCodeModeHost>, limits: CodeModeSessionCellExecutionLimits) -> Self {
fn with_host(host: Arc<OwnedCodeModeHost>, options: CodeModeSessionOptions) -> Self {
Self {
inner: Arc::new(SessionInner {
host,
limits,
options,
output_truncation: OnceLock::new(),
state: StdMutex::new(SessionState::New),
shutdown_requested: AtomicBool::new(false),
shutdown_result: StdMutex::new(None),
@@ -257,9 +273,13 @@ impl ProcessOwnedCodeModeSession {
self.inner.connection().await
}
fn server_output_budget(&self, budget: Option<usize>) -> Option<usize> {
budget.filter(|_| self.output_truncation() == CodeModeOutputTruncation::Server)
}
pub async fn execute(
&self,
request: ExecuteRequest,
mut request: ExecuteRequest,
delegate: Arc<dyn CodeModeSessionDelegate>,
preempt: Option<CancellationToken>,
) -> Result<StartedCell, String> {
@@ -268,6 +288,10 @@ impl ProcessOwnedCodeModeSession {
delegate,
trace: codex_otel::current_span_w3c_trace_context(),
});
// V1 encodes the exec budget as i32. Its maximum already exceeds the host collector limit.
request.max_output_tokens = self
.server_output_budget(request.max_output_tokens)
.map(|tokens| tokens.min(i32::MAX as usize));
binding
.connection
.execute(binding.remote, request, delegate, preempt)
@@ -276,22 +300,21 @@ impl ProcessOwnedCodeModeSession {
pub async fn wait(
&self,
request: WaitRequest,
mut request: WaitRequest,
preempt: Option<CancellationToken>,
) -> Result<WaitOutcome, String> {
let binding = self.connection().await?;
request.max_output_tokens = self.server_output_budget(request.max_output_tokens);
binding
.connection
.wait(binding.remote, request, preempt)
.await
}
pub async fn terminate(&self, request: TerminateRequest) -> Result<WaitOutcome, String> {
pub async fn terminate(&self, mut request: TerminateRequest) -> Result<WaitOutcome, String> {
let binding = self.connection().await?;
binding
.connection
.terminate(binding.remote, request.cell_id)
.await
request.max_output_tokens = self.server_output_budget(request.max_output_tokens);
binding.connection.terminate(binding.remote, request).await
}
pub async fn shutdown(&self) -> Result<(), String> {
@@ -352,7 +375,11 @@ impl SessionInner {
let result = match self.host.connection().await {
Ok(connection) => {
let cleanup = connection
.open_session(remote.clone(), self.limits.clone())
.open_session(
remote.clone(),
self.options.clone(),
&self.output_truncation,
)
.await;
cleanup.map(|cleanup| SessionBinding {
connection,
@@ -529,6 +556,14 @@ impl Default for ProcessOwnedCodeModeSession {
}
impl CodeModeSession for ProcessOwnedCodeModeSession {
fn output_truncation(&self) -> CodeModeOutputTruncation {
self.inner
.output_truncation
.get()
.copied()
.unwrap_or_default()
}
fn execute<'a>(
&'a self,
request: ExecuteRequest,