Remove the unused legacy remote compaction implementation (#44273)

## Why

Remote compaction already uses the normal Responses API, leaving the legacy `/responses/compact` runner unused.

## What changed

- Remove the legacy runner, endpoint client, request types, analytics variant, and obsolete test helpers and snapshots.
- Move tool-output trimming and its metadata tests into `compact_remote_history` for reuse by remote compaction v2.
- Consolidate retained-history filtering in the v2 implementation and remove redundant `RemoteCompactionV2` settings from tests.

## Testing

Extend the retained-history unit test to explicitly check that hook prompts survive compaction.

GitOrigin-RevId: 999bb1391aa22c3c281d6a96f98817d6c9e46ed9
This commit is contained in:
rka-oai
2026-09-09 18:40:53 +00:00
committed by copyberry
parent a3ba42b010
commit 1ac689cc7d
44 changed files with 169 additions and 1964 deletions
-1
View File
@@ -434,7 +434,6 @@ pub enum CompactionReason {
pub enum CompactionImplementation {
Responses,
ResponsesCompactionV2,
ResponsesCompact,
}
#[derive(Clone, Copy, Debug, Serialize)]
@@ -285,7 +285,6 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac
.enable_feature(Feature::GuardianApproval)
.enable_feature(Feature::GuardianReuseParentCompaction)
.disable_feature(Feature::EnableRequestCompression)
.enable_feature(Feature::RemoteCompactionV2)
.disable_feature(Feature::TokenBudget)
.with_extra_config(&format!(
"[mcp_servers.{TEST_SERVER_NAME}]\nurl = \"{mcp_url}/mcp\"\ndefault_tools_approval_mode = \"prompt\"\n\n[features.guardianv2]\nenabled = true\nthread_context = {thread_context_enabled}\npersist_scores = true\nreuse_parent_compaction = {reuse_parent_compaction}\n\n[features.guardianv2.review_scope]\ncomputer_use_only = false"
+2 -10
View File
@@ -2,7 +2,7 @@
Typed clients for Codex/OpenAI APIs built on top of the generic transport in `codex-client`.
- Hosts the request/response models and request builders for Responses and Compact APIs.
- Hosts the request/response models and request builders for Responses and related Codex APIs.
- Owns provider configuration (base URLs, headers, query params), auth header injection, retry tuning, and stream idle settings.
- Parses SSE streams into `ResponseEvent`/`ResponseStream`, including rate-limit snapshots and API-specific error mapping.
- Serves as the wire-level layer consumed by `codex-core`; higher layers handle auth refresh and business logic.
@@ -17,14 +17,6 @@ The public interface of this crate is intentionally small and uniform:
- `ResponsesOptions` for transport/header concerns (`conversation_id`, `session_source`, `extra_headers`, `compression`, `turn_state`).
- Output: a `ResponseStream` of `ResponseEvent` (both re-exported from `common`).
- **Compaction endpoint**
- Input: `CompactionInput<'a>` (re-exported as `codex_api::CompactionInput`):
- `model: &str`.
- `input: &[ResponseItem]` – history to compact.
- `instructions: &str` – fully-resolved compaction instructions.
- Output: `Vec<ResponseItem>`.
- `CompactClient::compact_input(&CompactionInput, extra_headers)` wraps the JSON encoding and retry/telemetry wiring.
- **Memory summarize endpoint**
- Input: `MemorySummarizeInput` (re-exported as `codex_api::MemorySummarizeInput`):
- `model: String`.
@@ -34,4 +26,4 @@ The public interface of this crate is intentionally small and uniform:
- Output: `Vec<MemorySummarizeOutput>`.
- `MemoriesClient::summarize_input(&MemorySummarizeInput, extra_headers)` wraps JSON encoding and retry/telemetry wiring.
All HTTP details (URLs, headers, retry/backoff policies, SSE framing) are encapsulated in `codex-api` and `codex-client`. Callers construct prompts/inputs using protocol types and work with typed streams of `ResponseEvent` or compacted `ResponseItem` values.
All HTTP details (URLs, headers, retry/backoff policies, SSE framing) are encapsulated in `codex-api` and `codex-client`. Callers construct prompts/inputs using protocol types and work with typed streams of `ResponseEvent` or other endpoint-specific response values.
-22
View File
@@ -43,28 +43,6 @@ impl From<CyberAccessProgram> for AccessPrograms {
}
}
/// Canonical input payload for the compaction endpoint.
#[derive(Debug, Clone, Serialize)]
pub struct CompactionInput<'a> {
pub model: &'a str,
pub input: &'a [ResponseItem],
#[serde(skip_serializing_if = "str::is_empty")]
pub instructions: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<ResponsesApiTools>,
pub parallel_tool_calls: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<Reasoning>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_cache_key: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<TextControls>,
#[serde(skip_serializing_if = "Option::is_none")]
pub access_programs: Option<AccessPrograms>,
}
/// Canonical input payload for the memory summarize endpoint.
#[derive(Debug, Clone, Serialize)]
pub struct MemorySummarizeInput {
-115
View File
@@ -1,115 +0,0 @@
use crate::auth::SharedAuthProvider;
use crate::common::CompactionInput;
use crate::endpoint::session::EndpointSession;
use crate::error::ApiError;
use crate::provider::Provider;
use codex_client::HttpTransport;
use codex_client::RequestTelemetry;
use codex_protocol::models::ResponseItem;
use http::HeaderMap;
use http::Method;
use serde::Deserialize;
use std::sync::Arc;
use std::sync::OnceLock;
use std::time::Duration;
const X_CODEX_TURN_STATE_HEADER: &str = "x-codex-turn-state";
pub struct CompactClient<T: HttpTransport> {
session: EndpointSession<T>,
}
impl<T: HttpTransport> CompactClient<T> {
pub fn new(transport: T, provider: Provider, auth: SharedAuthProvider) -> Self {
Self {
session: EndpointSession::new(transport, provider, auth),
}
}
pub fn with_telemetry(self, request: Option<Arc<dyn RequestTelemetry>>) -> Self {
Self {
session: self.session.with_request_telemetry(request),
}
}
fn path() -> &'static str {
"responses/compact"
}
pub async fn compact(
&self,
body: serde_json::Value,
extra_headers: HeaderMap,
request_timeout: Duration,
turn_state: Option<&OnceLock<String>>,
) -> Result<Vec<ResponseItem>, ApiError> {
let resp = self
.session
.execute_with(
Method::POST,
Self::path(),
extra_headers,
Some(body),
|req| {
req.timeout = Some(request_timeout);
},
)
.await?;
if let Some(turn_state) = turn_state
&& let Some(header_value) = resp
.headers
.get(X_CODEX_TURN_STATE_HEADER)
.and_then(|value| value.to_str().ok())
{
let _ = turn_state.set(header_value.to_string());
}
let parsed: CompactHistoryResponse =
serde_json::from_slice(&resp.body).map_err(|e| ApiError::Stream(e.to_string()))?;
Ok(parsed.output)
}
pub async fn compact_input(
&self,
input: &CompactionInput<'_>,
extra_headers: HeaderMap,
request_timeout: Duration,
turn_state: Option<&OnceLock<String>>,
) -> Result<Vec<ResponseItem>, ApiError> {
let body = serde_json::to_value(input)
.map_err(|e| ApiError::Stream(format!("failed to encode compaction input: {e}")))?;
self.compact(body, extra_headers, request_timeout, turn_state)
.await
}
}
#[derive(Debug, Deserialize)]
struct CompactHistoryResponse {
output: Vec<ResponseItem>,
}
#[cfg(test)]
mod tests {
use super::*;
use codex_client::Request;
use codex_client::Response;
use codex_client::StreamResponse;
use codex_client::TransportError;
#[derive(Clone, Default)]
struct DummyTransport;
impl HttpTransport for DummyTransport {
async fn execute(&self, _req: Request) -> Result<Response, TransportError> {
Err(TransportError::Build("execute should not run".to_string()))
}
async fn stream(&self, _req: Request) -> Result<StreamResponse, TransportError> {
Err(TransportError::Build("stream should not run".to_string()))
}
}
#[test]
fn path_is_responses_compact() {
assert_eq!(CompactClient::<DummyTransport>::path(), "responses/compact");
}
}
-2
View File
@@ -1,4 +1,3 @@
pub(crate) mod compact;
pub(crate) mod images;
pub(crate) mod memories;
pub(crate) mod models;
@@ -9,7 +8,6 @@ pub(crate) mod responses_websocket;
pub(crate) mod search;
mod session;
pub use compact::CompactClient;
pub use images::ImagesClient;
pub use memories::MemoriesClient;
pub use models::ModelsClient;
-2
View File
@@ -28,7 +28,6 @@ pub use crate::auth::AuthProviderFuture;
pub use crate::auth::SharedAuthProvider;
pub use crate::auth::auth_header_telemetry;
pub use crate::common::AccessPrograms;
pub use crate::common::CompactionInput;
pub use crate::common::MemorySummarizeInput;
pub use crate::common::MemorySummarizeOutput;
pub use crate::common::OpenAiVerbosity;
@@ -50,7 +49,6 @@ pub use crate::common::WS_REQUEST_HEADER_TRACEPARENT_CLIENT_METADATA_KEY;
pub use crate::common::WS_REQUEST_HEADER_TRACESTATE_CLIENT_METADATA_KEY;
pub use crate::common::create_text_param_for_request;
pub use crate::common::response_create_client_metadata;
pub use crate::endpoint::CompactClient;
pub use crate::endpoint::ImagesClient;
pub use crate::endpoint::MemoriesClient;
pub use crate::endpoint::ModelsClient;
-136
View File
@@ -34,8 +34,6 @@ use async_channel::Sender;
use codex_api::AgentIdentityTelemetry;
use codex_api::ApiError;
use codex_api::AuthProvider;
use codex_api::CompactClient as ApiCompactClient;
use codex_api::CompactionInput as ApiCompactionInput;
use codex_api::Compression;
use codex_api::MemoriesClient as ApiMemoriesClient;
use codex_api::MemorySummarizeInput as ApiMemorySummarizeInput;
@@ -90,7 +88,6 @@ use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::InternalSessionSource;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::W3cTraceContext;
use codex_rollout_trace::CompactionTraceContext;
use codex_rollout_trace::InferenceTraceAttempt;
use codex_rollout_trace::InferenceTraceContext;
use codex_tools::create_tools_json_for_responses_api;
@@ -168,21 +165,11 @@ const RESPONSES_WEBSOCKETS_V2_BETA_HEADER_VALUE: &str = "responses_websockets=20
const X_OPENAI_INTERNAL_CODEX_RESPONSES_LITE_HEADER: &str =
"x-openai-internal-codex-responses-lite";
const REALTIME_CALLS_ENDPOINT: &str = "/realtime/calls";
const RESPONSES_COMPACT_ENDPOINT: &str = "/responses/compact";
// `/responses/compact` is unary, so the timeout covers the full response rather than one idle
// period between stream events.
const COMPACT_REQUEST_TIMEOUT_IDLE_MULTIPLIER: u32 = 4;
const MEMORIES_SUMMARIZE_ENDPOINT: &str = "/memories/trace_summarize";
#[cfg(test)]
pub(crate) const WEBSOCKET_CONNECT_TIMEOUT: Duration =
Duration::from_millis(DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS);
pub(crate) struct CompactConversationRequestSettings {
pub(crate) effort: Option<ReasoningEffortConfig>,
pub(crate) summary: ReasoningSummaryConfig,
pub(crate) service_tier: Option<String>,
}
fn session_telemetry_for_request(
session_telemetry: &SessionTelemetry,
request: &ResponsesApiRequest,
@@ -569,124 +556,6 @@ impl ModelClient {
activated
}
/// Compacts the current conversation history using the Compact endpoint.
///
/// This is a unary call (no streaming) that returns a new list of
/// `ResponseItem`s representing the compacted transcript.
///
/// The model selection and telemetry context are passed explicitly to keep `ModelClient`
/// session-scoped.
#[allow(clippy::too_many_arguments)]
pub(crate) async fn compact_conversation_history(
&self,
prompt: &Prompt,
model_info: &ModelInfo,
turn_state: Option<Arc<OnceLock<String>>>,
settings: CompactConversationRequestSettings,
session_telemetry: &SessionTelemetry,
compaction_trace: &CompactionTraceContext,
responses_metadata: &CodexResponsesMetadata,
) -> Result<Vec<ResponseItem>> {
if prompt.input.is_empty() {
return Ok(Vec::new());
}
let client_setup = self.current_client_setup().await?;
let transport =
self.build_api_transport(&client_setup.api_provider, RESPONSES_COMPACT_ENDPOINT)?;
let request_telemetry = Self::build_request_telemetry(
session_telemetry,
AuthRequestTelemetryContext::new(
client_setup.auth.as_ref().map(CodexAuth::auth_mode),
client_setup.api_auth.as_ref(),
client_setup.agent_identity_telemetry.clone(),
PendingUnauthorizedRetry::default(),
),
RequestRouteTelemetry::for_endpoint(RESPONSES_COMPACT_ENDPOINT),
self.state.auth_env_telemetry.clone(),
);
let request = self.build_responses_request(
prompt,
model_info,
settings.effort,
settings.summary,
settings.service_tier,
responses_metadata,
)?;
let ResponsesApiRequest {
model,
instructions,
mut input,
tools,
parallel_tool_calls,
reasoning,
service_tier,
prompt_cache_key,
text,
..
} = request;
self.prepare_response_items_for_request(&mut input);
let payload = ApiCompactionInput {
model: &model,
input: &input,
instructions: &instructions,
tools,
parallel_tool_calls,
reasoning,
service_tier: service_tier.as_deref(),
prompt_cache_key: prompt_cache_key.as_deref(),
text,
access_programs: cyber_access_program::for_auth(
client_setup.auth.as_ref(),
prompt.cyber_access_program,
),
};
let mut extra_headers = ApiHeaderMap::new();
if let Ok(header_value) = HeaderValue::from_str(&responses_metadata.installation_id) {
extra_headers.insert(X_CODEX_INSTALLATION_ID_HEADER, header_value);
}
extra_headers.extend(build_responses_headers(
self.state.beta_features_header.as_deref(),
turn_state.as_ref(),
));
add_originator_header(&mut extra_headers, self.state.originator.as_str());
extra_headers.extend(self.build_responses_compatibility_headers(responses_metadata));
extra_headers.extend(build_session_headers(
Some(responses_metadata.session_id.to_string()),
Some(responses_metadata.thread_id.to_string()),
));
if let Some(header_value) = self.generate_attestation_header_for().await {
extra_headers.insert(X_OAI_ATTESTATION_HEADER, header_value);
}
if let Some(header_value) = self.build_routing_hint_header(
client_setup.auth.as_ref(),
&model,
service_tier.as_deref(),
) {
extra_headers.insert(X_CODEX_ROUTING_HINT_HEADER, header_value);
}
add_responses_lite_header(&mut extra_headers, model_info.use_responses_lite);
let compact_request_timeout = client_setup
.api_provider
.stream_idle_timeout
.saturating_mul(COMPACT_REQUEST_TIMEOUT_IDLE_MULTIPLIER);
let client =
ApiCompactClient::new(transport, client_setup.api_provider, client_setup.api_auth)
.with_telemetry(Some(request_telemetry));
let trace_attempt = compaction_trace.start_attempt(&payload);
let result = client
.compact_input(
&payload,
extra_headers,
compact_request_timeout,
turn_state.as_deref(),
)
.await
.map_err(|error| self.state.provider.map_api_error(error));
trace_attempt.record_result(result.as_deref());
result
}
pub(crate) async fn create_realtime_call_with_headers(
&self,
sdp: String,
@@ -1278,11 +1147,6 @@ impl Drop for ModelClientSession {
}
impl ModelClientSession {
#[expect(dead_code, reason = "the v1-only accessor is removed in the follow-up")]
pub(crate) fn turn_state(&self) -> Arc<OnceLock<String>> {
Arc::clone(&self.turn_state)
}
fn reset_websocket_session(&mut self) {
self.websocket_session.connection = None;
self.websocket_session.endpoint = None;
-161
View File
@@ -1,5 +1,4 @@
use super::AuthRequestTelemetryContext;
use super::CompactConversationRequestSettings;
use super::ModelClient;
use super::PendingUnauthorizedRetry;
use super::Prompt;
@@ -22,8 +21,6 @@ use codex_api::ResponsesEndpoint;
use codex_api::TransportError;
use codex_http_client::HttpClientFactory;
use codex_http_client::OutboundProxyPolicy;
use codex_login::AuthCredentialsStoreMode;
use codex_login::AuthKeyringBackendKind;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_login::auth::AgentIdentityAuthPolicy;
@@ -55,7 +52,6 @@ use codex_protocol::openai_models::ReasoningEffortPreset;
use codex_protocol::protocol::InternalSessionSource;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_rollout_trace::CompactionTraceContext;
use codex_rollout_trace::ExecutionStatus;
use codex_rollout_trace::InferenceTraceAttempt;
use codex_rollout_trace::InferenceTraceContext;
@@ -87,13 +83,7 @@ use tracing_subscriber::layer::Context as LayerContext;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::util::SubscriberInitExt;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::method;
use wiremock::matchers::path;
const TEST_CHATGPT_ID_TOKEN: &str = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS9hdXRoIjp7ImNoYXRncHRfdXNlcl9pZCI6InVzZXItMTIzNDUiLCJ1c2VyX2lkIjoidXNlci0xMjM0NSIsImNoYXRncHRfcGxhbl90eXBlIjoicHJvIiwiY2hhdGdwdF9hY2NvdW50X2lkIjoiYWNjb3VudC0xMjMifX0.c2ln";
const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111";
fn test_model_client(session_source: SessionSource) -> ModelClient {
@@ -123,118 +113,6 @@ fn test_model_client_with_thread_id(
)
}
#[tokio::test]
async fn compact_uses_bearer_after_agent_identity_session_fallback() -> anyhow::Result<()> {
let server = MockServer::start().await;
let registration_count = Arc::new(AtomicUsize::new(0));
let response_count = Arc::clone(&registration_count);
Mock::given(method("POST"))
.and(path("/v1/agent/register"))
.respond_with(move |_request: &wiremock::Request| {
response_count.fetch_add(1, Ordering::SeqCst);
ResponseTemplate::new(/*status*/ 503)
})
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/v1/responses/compact"))
.respond_with(ResponseTemplate::new(/*status*/ 200).set_body_json(json!({
"output": []
})))
.expect(/*requests*/ 1)
.mount(&server)
.await;
let codex_home = TempDir::new()?;
let auth_manager = chatgpt_auth_manager(&codex_home, server.uri()).await;
let mut provider = ModelProviderInfo::create_openai_provider(/*base_url*/ None);
provider.base_url = Some(format!("{}/v1", server.uri()));
provider.supports_websockets = false;
let thread_id = ThreadId::new();
let client = ModelClient::new(
Some(auth_manager),
AgentIdentityAuthPolicy::ChatGptAuth,
thread_id,
provider,
SessionSource::Cli,
"test_originator".to_string(),
/*model_verbosity*/ None,
/*content_item_kinds_enabled*/ true,
/*enable_request_compression*/ false,
/*include_timing_metrics*/ false,
/*beta_features_header*/ None,
/*concurrent_reasoning_summaries_enabled*/ false,
/*attestation_provider*/ None,
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
);
let prompt = Prompt {
input: vec![ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "please compact".to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
}],
base_instructions: BaseInstructions {
text: "base instructions".to_string(),
provenance: None,
},
..Default::default()
};
let responses_metadata = test_responses_metadata_for_client(
&client,
/*turn_id*/ None,
format!("{}:0", client.state.thread_id),
/*parent_thread_id*/ None,
TestCodexResponsesRequestKind::Turn,
);
let output = client
.compact_conversation_history(
&prompt,
&test_model_info(),
/*turn_state*/ None,
CompactConversationRequestSettings {
effort: None,
summary: codex_protocol::config_types::ReasoningSummary::None,
service_tier: None,
},
&test_session_telemetry(),
&CompactionTraceContext::disabled(),
&responses_metadata,
)
.await?;
assert!(output.is_empty());
assert_eq!(registration_count.load(Ordering::SeqCst), 3);
let requests = server
.received_requests()
.await
.expect("server should record requests");
let compact_request = requests
.iter()
.find(|request| request.url.path() == "/v1/responses/compact")
.expect("compact request should be captured");
assert_eq!(
compact_request
.headers
.get(http::header::AUTHORIZATION)
.and_then(|value| value.to_str().ok()),
Some("Bearer test-access-token")
);
assert_eq!(
compact_request
.headers
.get("ChatGPT-Account-ID")
.and_then(|value| value.to_str().ok()),
Some("account-123")
);
Ok(())
}
fn test_model_provider() -> SharedModelProvider {
test_model_client(SessionSource::Cli).state.provider.clone()
}
@@ -500,45 +378,6 @@ fn reasoning_effort_for_requests_preserves_non_ultra_and_persistent_behavior() {
);
}
fn write_chatgpt_auth_json(codex_home: &std::path::Path) {
let auth_json = json!({
"tokens": {
"id_token": TEST_CHATGPT_ID_TOKEN,
"access_token": "test-access-token",
"refresh_token": "test-refresh-token",
"account_id": "account-123"
},
"last_refresh": "2099-01-01T00:00:00Z"
});
std::fs::write(
codex_home.join("auth.json"),
serde_json::to_string_pretty(&auth_json).expect("serialize auth.json"),
)
.expect("write auth.json");
}
async fn chatgpt_auth_manager(
codex_home: &TempDir,
agent_identity_authapi_base_url: String,
) -> Arc<AuthManager> {
write_chatgpt_auth_json(codex_home.path());
let auth_manager = AuthManager::shared(
codex_home.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
/*forced_chatgpt_workspace_id*/ None,
/*chatgpt_base_url*/ None,
AuthKeyringBackendKind::default(),
codex_login::test_support::transport_default_auth_route_config(),
)
.await;
let auth = auth_manager.auth().await.expect("auth should load");
AuthManager::from_auth_for_testing_with_agent_identity_authapi_base_url(
auth,
agent_identity_authapi_base_url,
)
}
#[derive(Default)]
struct TagCollectorVisitor {
tags: BTreeMap<String, String>,
-2
View File
@@ -15,8 +15,6 @@ use crate::hook_runtime::run_pre_compact_hooks;
use crate::responses_metadata::CodexResponsesMetadata;
use crate::responses_metadata::CodexResponsesRequestKind;
use crate::responses_metadata::CompactionTurnMetadata;
#[cfg(test)]
use crate::session::PreviousTurnSettings;
use crate::session::RequestEffortUsage;
use crate::session::session::Session;
use crate::session::step_context::StepContext;
@@ -36,7 +36,6 @@ pub(crate) fn record_model_fallback(
let implementation_tag = match implementation {
CompactionImplementation::Responses => "responses",
CompactionImplementation::ResponsesCompactionV2 => "responses_compaction_v2",
CompactionImplementation::ResponsesCompact => "responses_compact",
};
let outcome = if fallback_error.is_none() {
"succeeded"
-525
View File
@@ -1,525 +0,0 @@
use std::sync::Arc;
use std::sync::OnceLock;
use crate::compact::CompactedHistoryMetadata;
use crate::compact::CompactionAnalyticsAttempt;
use crate::compact::CompactionAnalyticsDetails;
use crate::compact::InitialContextInjection;
use crate::compact::build_compaction_initial_context;
use crate::compact::compaction_status_from_result;
use crate::compact::insert_initial_context_before_last_real_user_or_summary;
use crate::compact_model_fallback::record_model_fallback;
use crate::compact_model_fallback::should_retry_with_current_model;
use crate::compact_remote_history::HistoryItemGroup;
use crate::compact_remote_history::history_item_groups;
use crate::context::world_state::WorldState;
use crate::context_manager::ContextManager;
use crate::context_manager::estimate_item_token_count;
use crate::hook_runtime::PostCompactHookOutcome;
use crate::hook_runtime::PreCompactHookOutcome;
use crate::hook_runtime::run_post_compact_hooks;
use crate::hook_runtime::run_pre_compact_hooks;
use crate::responses_metadata::CompactionTurnMetadata;
use crate::session::session::Session;
use crate::session::step_context::StepContext;
use crate::session::turn_context::TurnContext;
use codex_analytics::CompactionImplementation;
use codex_analytics::CompactionPhase;
use codex_analytics::CompactionReason;
use codex_analytics::CompactionTrigger;
use codex_history::ResponseItemEnvelope;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::items::ContextCompactionItem;
use codex_protocol::items::TurnItem;
use codex_protocol::models::BaseInstructions;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::TurnStartedEvent;
use codex_rollout_trace::CompactionCheckpointTracePayload;
use codex_utils_output_truncation::approx_token_count;
use tokio_util::sync::CancellationToken;
#[path = "compact_remote_request.rs"]
mod request;
use request::RemoteCompactAttempt;
use request::run_remote_compact_attempt;
const CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE: &str =
"Output exceeded the available model context and was truncated";
pub(crate) async fn run_inline_remote_auto_compact_task(
sess: Arc<Session>,
step_context: Arc<StepContext>,
fallback_step_context: Option<Arc<StepContext>>,
turn_state: Arc<OnceLock<String>>,
initial_context_injection: InitialContextInjection,
reason: CompactionReason,
phase: CompactionPhase,
) -> CodexResult<()> {
let compaction_metadata = CompactionTurnMetadata::new(
CompactionTrigger::Auto,
reason,
CompactionImplementation::ResponsesCompact,
phase,
);
run_remote_compact_task_inner(
&sess,
&step_context,
fallback_step_context.as_ref(),
Some(turn_state),
initial_context_injection,
compaction_metadata,
)
.await?;
Ok(())
}
pub(crate) async fn run_remote_compact_task(
sess: Arc<Session>,
turn_context: Arc<TurnContext>,
) -> CodexResult<()> {
// Standalone compaction is its own request boundary, so it captures a fresh step.
let step_context = sess
.capture_step_context(Arc::clone(&turn_context), &CancellationToken::new())
.await?;
let start_event = EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_context.sub_id.clone(),
trace_id: turn_context.trace_id.clone(),
started_at: turn_context.turn_timing_state.started_at_unix_secs().await,
model_context_window: turn_context.model_context_window(),
collaboration_mode_kind: turn_context.mode(),
});
sess.send_event(&turn_context, start_event).await;
let compaction_metadata = CompactionTurnMetadata::new(
CompactionTrigger::Manual,
CompactionReason::UserRequested,
CompactionImplementation::ResponsesCompact,
CompactionPhase::StandaloneTurn,
);
run_remote_compact_task_inner(
&sess,
&step_context,
/*fallback_step_context*/ None,
/*turn_state*/ None,
InitialContextInjection::DoNotInject,
compaction_metadata,
)
.await?;
Ok(())
}
async fn run_remote_compact_task_inner(
sess: &Arc<Session>,
step_context: &Arc<StepContext>,
fallback_step_context: Option<&Arc<StepContext>>,
turn_state: Option<Arc<OnceLock<String>>>,
initial_context_injection: InitialContextInjection,
compaction_metadata: CompactionTurnMetadata,
) -> CodexResult<()> {
let turn_context = &step_context.turn;
let trigger = compaction_metadata.trigger();
let reason = compaction_metadata.reason();
let implementation = compaction_metadata.implementation();
let phase = compaction_metadata.phase();
let mut analytics_details = CompactionAnalyticsDetails {
active_context_tokens_before: Some(sess.get_total_token_usage().await),
..Default::default()
};
let attempt = CompactionAnalyticsAttempt::begin(
sess.as_ref(),
turn_context.as_ref(),
trigger,
reason,
implementation,
phase,
)
.await;
let pre_compact_outcome = run_pre_compact_hooks(sess, turn_context, trigger).await;
match pre_compact_outcome {
PreCompactHookOutcome::Continue => {}
PreCompactHookOutcome::Stopped => {
let error = CodexErr::TurnAborted;
attempt
.track(
sess.as_ref(),
codex_analytics::CompactionStatus::Interrupted,
Some(&error),
analytics_details,
)
.await;
return Err(error);
}
}
let result = run_remote_compact_task_inner_impl(
sess,
step_context,
fallback_step_context,
turn_state,
initial_context_injection,
compaction_metadata,
&mut analytics_details,
)
.await;
let status = compaction_status_from_result(&result);
let codex_error = result.as_ref().err();
if result.is_ok() {
let post_compact_outcome = run_post_compact_hooks(sess, turn_context, trigger).await;
if let PostCompactHookOutcome::Stopped = post_compact_outcome {
attempt
.track(sess.as_ref(), status, codex_error, analytics_details)
.await;
return Err(CodexErr::TurnAborted);
}
}
attempt
.track(sess.as_ref(), status, codex_error, analytics_details)
.await;
if let Err(err) = result {
sess.track_turn_codex_error(turn_context, &err);
let event = EventMsg::Error(
err.to_error_event(Some("Error running remote compact task".to_string())),
);
sess.send_event(turn_context, event).await;
return Err(err);
}
Ok(())
}
async fn run_remote_compact_task_inner_impl(
sess: &Arc<Session>,
step_context: &Arc<StepContext>,
fallback_step_context: Option<&Arc<StepContext>>,
turn_state: Option<Arc<OnceLock<String>>>,
initial_context_injection: InitialContextInjection,
compaction_metadata: CompactionTurnMetadata,
analytics_details: &mut CompactionAnalyticsDetails,
) -> CodexResult<()> {
let turn_context = &step_context.turn;
let context_compaction_item = ContextCompactionItem::new();
let compaction_id = context_compaction_item.id.clone();
// Use the UI compaction item ID as the trace compaction ID so protocol lifecycle events,
// endpoint attempts, and the installed history checkpoint all have one join key.
let compaction_trace = sess.services.rollout_thread_trace.compaction_trace_context(
turn_context.sub_id.as_str(),
compaction_id.as_str(),
turn_context.model_info().slug.as_str(),
turn_context.provider.info().name.as_str(),
);
let compaction_item = TurnItem::ContextCompaction(context_compaction_item);
sess.emit_turn_item_started(turn_context, &compaction_item)
.await;
let attempt = run_remote_compact_attempt(
sess,
step_context,
turn_state.clone(),
&compaction_trace,
compaction_metadata,
analytics_details,
)
.await;
let (attempt, compaction_turn_context) = match attempt {
Ok(attempt) => (attempt, turn_context),
Err(error) => {
let Some(fallback_step_context) = fallback_step_context else {
return Err(error);
};
if !should_retry_with_current_model(&error) {
return Err(error);
}
sess.set_last_known_step_context(fallback_step_context)
.await;
let fallback_turn_context = &fallback_step_context.turn;
let fallback_compaction_trace =
sess.services.rollout_thread_trace.compaction_trace_context(
fallback_turn_context.sub_id.as_str(),
compaction_id.as_str(),
fallback_turn_context.model_info().slug.as_str(),
fallback_turn_context.provider.info().name.as_str(),
);
let fallback_result = run_remote_compact_attempt(
sess,
fallback_step_context,
turn_state,
&fallback_compaction_trace,
compaction_metadata,
analytics_details,
)
.await;
record_model_fallback(
&sess.services.session_telemetry,
turn_context.model_info().slug.as_str(),
fallback_turn_context.model_info().slug.as_str(),
compaction_metadata.reason(),
compaction_metadata.implementation(),
fallback_result.as_ref().err(),
);
match fallback_result {
Ok(attempt) => (attempt, fallback_turn_context),
Err(_) => return Err(error),
}
}
};
let RemoteCompactAttempt {
new_history,
trace_input_history,
} = attempt;
let (new_window_number, new_window_ids) = sess.advance_auto_compact_window().await;
let (new_history, world_state_baseline) =
process_compacted_history(sess.as_ref(), new_history, &initial_context_injection).await;
let reference_context_item = match initial_context_injection {
InitialContextInjection::DoNotInject => None,
InitialContextInjection::BeforeLastUserMessage { step_context, .. } => {
Some(step_context.to_turn_context_item())
}
};
// Install is the semantic boundary where the compact endpoint's output becomes live
// thread history. Keep it distinct from the later inference request so the reducer can
// still represent repeated developer/context prefix items exactly as the model saw them.
if let Some(trace_input_history) = trace_input_history.as_deref() {
compaction_trace.record_installed(&CompactionCheckpointTracePayload {
input_history: trace_input_history,
replacement_history: &new_history,
});
}
// Legacy `/responses/compact` returns provider-normalized items without a stable link to their
// original envelopes, so it does not preserve harness metadata. Compaction-trigger/v2 does.
let new_history = new_history
.into_iter()
.map(ResponseItemEnvelope::new)
.collect();
sess.replace_compacted_history(
new_history,
reference_context_item,
world_state_baseline,
CompactedHistoryMetadata {
message: String::new(),
window_number: new_window_number,
window_ids: new_window_ids,
compaction_response_id: None,
compaction_model_hash: compaction_turn_context.model_info().comp_hash.clone(),
},
)
.await;
sess.recompute_token_usage(compaction_turn_context).await;
sess.emit_turn_item_completed(compaction_turn_context, compaction_item)
.await;
Ok(())
}
pub(crate) async fn process_compacted_history(
sess: &Session,
compacted_history: Vec<ResponseItem>,
initial_context_injection: &InitialContextInjection,
) -> (Vec<ResponseItem>, Option<Arc<WorldState>>) {
let compacted_history = compacted_history
.into_iter()
.map(ResponseItemEnvelope::new)
.collect();
let (compacted_history, world_state_baseline) =
process_annotated_compacted_history(sess, compacted_history, initial_context_injection)
.await;
(
compacted_history
.into_iter()
.map(ResponseItemEnvelope::into_item)
.collect(),
world_state_baseline,
)
}
/// Installs already-annotated remote compaction output without dropping its metadata sidecar.
pub(crate) async fn process_annotated_compacted_history(
sess: &Session,
compacted_history: Vec<ResponseItemEnvelope>,
initial_context_injection: &InitialContextInjection,
) -> (Vec<ResponseItemEnvelope>, Option<Arc<WorldState>>) {
// Mid-turn compaction is the only path that must inject initial context above the last user
// message in the replacement history. Pre-turn compaction instead injects context after the
// compaction item, but mid-turn compaction keeps the compaction item last for model training.
let (initial_context, world_state_baseline) =
build_compaction_initial_context(sess, initial_context_injection).await;
let compacted_history = history_item_groups(compacted_history)
.filter(|group| should_keep_compacted_history_item(&group.source.item))
.flat_map(HistoryItemGroup::into_items)
.collect();
(
insert_initial_context_before_last_real_user_or_summary(compacted_history, initial_context),
world_state_baseline,
)
}
/// Returns whether an item from remote compaction output should be preserved.
///
/// Called while processing the model-provided compacted transcript, before we
/// append fresh canonical context from the current session.
///
/// We drop:
/// - `developer` messages because remote output can include stale/duplicated
/// instruction content.
/// - non-user-content `user` messages (session prefix/instruction wrappers),
/// while preserving real user messages and persisted hook prompts.
///
/// This intentionally keeps:
/// - `assistant` messages (future remote compaction models may emit them)
/// - `user`-role warnings that parse as `TurnItem::UserMessage` and compaction-generated summary
/// messages. Legacy warning fragments are filtered by `parse_turn_item` before they reach this
/// check.
pub(crate) fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
match item {
ResponseItem::Message { role, .. } if role == "developer" => false,
ResponseItem::Message { role, .. } if role == "user" => {
matches!(
crate::event_mapping::parse_turn_item(item),
Some(TurnItem::UserMessage(_) | TurnItem::HookPrompt(_))
)
}
ResponseItem::Message { role, .. } if role == "assistant" => true,
ResponseItem::Message { .. } => false,
ResponseItem::AgentMessage { .. } => true,
ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true,
ResponseItem::ConfigurationUpdate { .. } | ResponseItem::CompactionTrigger { .. } => false,
ResponseItem::AdditionalTools { .. }
| ResponseItem::Reasoning { .. }
| ResponseItem::LocalShellCall { .. }
| ResponseItem::FunctionCall { .. }
| ResponseItem::ToolSearchCall { .. }
| ResponseItem::FunctionCallOutput { .. }
| ResponseItem::ToolSearchOutput { .. }
| ResponseItem::CustomToolCall { .. }
| ResponseItem::CustomToolCallOutput { .. }
| ResponseItem::WebSearchCall { .. }
| ResponseItem::ImageGenerationCall { .. }
| ResponseItem::Other => false,
}
}
pub(crate) fn trim_function_call_history_to_fit_context_window(
history: &mut ContextManager,
turn_context: &TurnContext,
base_instructions: &BaseInstructions,
) -> (usize, i64) {
let Some(context_window) = turn_context.model_context_window() else {
return (0, 0);
};
// Keep the unclamped total so replacing an item cannot lose an overflow hidden by i64
// saturation in the normal history estimator.
let base_tokens =
i128::try_from(approx_token_count(&base_instructions.text)).unwrap_or(i128::MAX);
let original_items = history.annotated_items();
let mut estimated_tokens = history_item_groups(original_items.iter().map(|item| &item.item))
.map(|group| group.estimated_token_count())
.fold(base_tokens, i128::saturating_add);
let initial_estimated_tokens = i64::try_from(estimated_tokens).unwrap_or(i64::MAX);
let mut rewritten_items = Vec::new();
let mut consumed_items: usize = 0;
for group in history_item_groups(original_items.iter().map(|item| &item.item))
.collect::<Vec<_>>()
.into_iter()
.rev()
{
if i64::try_from(estimated_tokens).unwrap_or(i64::MAX) <= context_window {
break;
}
let group_item_count = 1 + usize::from(group.attached_notice.is_some());
let source_index = original_items
.len()
.saturating_sub(consumed_items.saturating_add(group_item_count));
let Some(rewritten_item) = original_items
.get(source_index)
.and_then(rewritten_output_for_context_window)
else {
break;
};
estimated_tokens = estimated_tokens
.saturating_sub(group.estimated_token_count())
.saturating_add(i128::from(estimate_item_token_count(&rewritten_item.item)));
consumed_items += group_item_count;
rewritten_items.push(rewritten_item);
}
let rewritten_outputs = rewritten_items.len();
if rewritten_outputs > 0 {
let retained_len = original_items.len() - consumed_items;
let mut items = original_items[..retained_len].to_vec();
items.extend(rewritten_items.into_iter().rev());
history.replace_annotated(items);
}
let final_estimated_tokens = i64::try_from(estimated_tokens).unwrap_or(i64::MAX);
let estimated_deleted_tokens = initial_estimated_tokens.saturating_sub(final_estimated_tokens);
(rewritten_outputs, estimated_deleted_tokens)
}
fn rewritten_output_for_context_window(
envelope: &ResponseItemEnvelope,
) -> Option<ResponseItemEnvelope> {
let item = match &envelope.item {
ResponseItem::FunctionCallOutput {
id,
call_id,
name,
namespace,
output,
internal_chat_message_metadata_passthrough: metadata,
} => ResponseItem::FunctionCallOutput {
id: id.clone(),
call_id: call_id.clone(),
name: name.clone(),
namespace: namespace.clone(),
output: truncated_output_payload(output),
internal_chat_message_metadata_passthrough: metadata.clone(),
},
ResponseItem::CustomToolCallOutput {
id,
call_id,
name,
output,
internal_chat_message_metadata_passthrough: metadata,
} => ResponseItem::CustomToolCallOutput {
id: id.clone(),
call_id: call_id.clone(),
name: name.clone(),
output: truncated_output_payload(output),
internal_chat_message_metadata_passthrough: metadata.clone(),
},
ResponseItem::ToolSearchOutput {
id,
call_id,
status,
execution,
internal_chat_message_metadata_passthrough: metadata,
..
} => ResponseItem::ToolSearchOutput {
id: id.clone(),
call_id: call_id.clone(),
status: status.clone(),
execution: execution.clone(),
tools: Vec::new(),
internal_chat_message_metadata_passthrough: metadata.clone(),
},
_ => return None,
};
Some(ResponseItemEnvelope {
item,
metadata: envelope.metadata.clone(),
})
}
fn truncated_output_payload(output: &FunctionCallOutputPayload) -> FunctionCallOutputPayload {
FunctionCallOutputPayload {
body: FunctionCallOutputBody::Text(CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE.to_string()),
success: output.success,
}
}
#[cfg(test)]
#[path = "compact_remote_metadata_tests.rs"]
mod metadata_tests;
+134
View File
@@ -2,9 +2,19 @@ use std::borrow::Borrow;
use crate::context::ContextualUserFragment;
use crate::context::ImageResizeNotice;
use crate::context_manager::ContextManager;
use crate::context_manager::estimate_item_token_count;
use crate::session::turn_context::TurnContext;
use codex_history::ResponseItemEnvelope;
use codex_protocol::models::BaseInstructions;
use codex_protocol::models::ContentItem;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseItem;
use codex_utils_output_truncation::approx_token_count;
const CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE: &str =
"Output exceeded the available model context and was truncated";
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct HistoryItemGroup<T> {
@@ -54,3 +64,127 @@ fn is_attached_notice(notice: &ResponseItem) -> bool {
)
)
}
pub(crate) fn trim_function_call_history_to_fit_context_window(
history: &mut ContextManager,
turn_context: &TurnContext,
base_instructions: &BaseInstructions,
) -> (usize, i64) {
let Some(context_window) = turn_context.model_context_window() else {
return (0, 0);
};
// Keep the unclamped total so replacing an item cannot lose an overflow hidden by i64
// saturation in the normal history estimator.
let base_tokens =
i128::try_from(approx_token_count(&base_instructions.text)).unwrap_or(i128::MAX);
let original_items = history.annotated_items();
let mut estimated_tokens = history_item_groups(original_items.iter().map(|item| &item.item))
.map(|group| group.estimated_token_count())
.fold(base_tokens, i128::saturating_add);
let initial_estimated_tokens = i64::try_from(estimated_tokens).unwrap_or(i64::MAX);
let mut rewritten_items = Vec::new();
let mut consumed_items: usize = 0;
for group in history_item_groups(original_items.iter().map(|item| &item.item))
.collect::<Vec<_>>()
.into_iter()
.rev()
{
if i64::try_from(estimated_tokens).unwrap_or(i64::MAX) <= context_window {
break;
}
let group_item_count = 1 + usize::from(group.attached_notice.is_some());
let source_index = original_items
.len()
.saturating_sub(consumed_items.saturating_add(group_item_count));
let Some(rewritten_item) = original_items
.get(source_index)
.and_then(rewritten_output_for_context_window)
else {
break;
};
estimated_tokens = estimated_tokens
.saturating_sub(group.estimated_token_count())
.saturating_add(i128::from(estimate_item_token_count(&rewritten_item.item)));
consumed_items += group_item_count;
rewritten_items.push(rewritten_item);
}
let rewritten_outputs = rewritten_items.len();
if rewritten_outputs > 0 {
let retained_len = original_items.len() - consumed_items;
let mut items = original_items[..retained_len].to_vec();
items.extend(rewritten_items.into_iter().rev());
history.replace_annotated(items);
}
let final_estimated_tokens = i64::try_from(estimated_tokens).unwrap_or(i64::MAX);
let estimated_deleted_tokens = initial_estimated_tokens.saturating_sub(final_estimated_tokens);
(rewritten_outputs, estimated_deleted_tokens)
}
fn rewritten_output_for_context_window(
envelope: &ResponseItemEnvelope,
) -> Option<ResponseItemEnvelope> {
let item = match &envelope.item {
ResponseItem::FunctionCallOutput {
id,
call_id,
name,
namespace,
output,
internal_chat_message_metadata_passthrough: metadata,
} => ResponseItem::FunctionCallOutput {
id: id.clone(),
call_id: call_id.clone(),
name: name.clone(),
namespace: namespace.clone(),
output: truncated_output_payload(output),
internal_chat_message_metadata_passthrough: metadata.clone(),
},
ResponseItem::CustomToolCallOutput {
id,
call_id,
name,
output,
internal_chat_message_metadata_passthrough: metadata,
} => ResponseItem::CustomToolCallOutput {
id: id.clone(),
call_id: call_id.clone(),
name: name.clone(),
output: truncated_output_payload(output),
internal_chat_message_metadata_passthrough: metadata.clone(),
},
ResponseItem::ToolSearchOutput {
id,
call_id,
status,
execution,
internal_chat_message_metadata_passthrough: metadata,
..
} => ResponseItem::ToolSearchOutput {
id: id.clone(),
call_id: call_id.clone(),
status: status.clone(),
execution: execution.clone(),
tools: Vec::new(),
internal_chat_message_metadata_passthrough: metadata.clone(),
},
_ => return None,
};
Some(ResponseItemEnvelope {
item,
metadata: envelope.metadata.clone(),
})
}
fn truncated_output_payload(output: &FunctionCallOutputPayload) -> FunctionCallOutputPayload {
FunctionCallOutputPayload {
body: FunctionCallOutputBody::Text(CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE.to_string()),
success: output.success,
}
}
#[cfg(test)]
#[path = "compact_remote_history_tests.rs"]
mod metadata_tests;
-109
View File
@@ -1,109 +0,0 @@
use std::sync::Arc;
use std::sync::OnceLock;
use super::trim_function_call_history_to_fit_context_window;
use crate::Prompt;
use crate::client::CompactConversationRequestSettings;
use crate::compact::CompactionAnalyticsDetails;
use crate::responses_metadata::CodexResponsesRequestKind;
use crate::responses_metadata::CompactionTurnMetadata;
use crate::session::RequestEffortUsage;
use crate::session::session::Session;
use crate::session::step_context::StepContext;
use codex_protocol::auth::AuthMode;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::models::ResponseItem;
use codex_rollout_trace::CompactionTraceContext;
use tracing::info;
pub(super) struct RemoteCompactAttempt {
pub(super) new_history: Vec<ResponseItem>,
pub(super) trace_input_history: Option<Vec<ResponseItem>>,
}
pub(super) async fn run_remote_compact_attempt(
sess: &Arc<Session>,
step_context: &Arc<StepContext>,
turn_state: Option<Arc<OnceLock<String>>>,
compaction_trace: &CompactionTraceContext,
compaction_metadata: CompactionTurnMetadata,
analytics_details: &mut CompactionAnalyticsDetails,
) -> CodexResult<RemoteCompactAttempt> {
let turn_context = &step_context.turn;
let mut history = sess.clone_history().await;
let base_instructions = sess.get_prompt_base_instructions().await;
let (rewritten_outputs, estimated_deleted_tokens) =
trim_function_call_history_to_fit_context_window(
&mut history,
turn_context.as_ref(),
&base_instructions,
);
if rewritten_outputs > 0 {
info!(
turn_id = %turn_context.sub_id,
rewritten_outputs,
"rewrote history outputs before remote compaction"
);
}
if estimated_deleted_tokens > 0 {
let max_local_deleted_tokens = sess
.estimated_tokens_after_last_model_generated_item()
.await;
analytics_details.active_context_tokens_before = analytics_details
.active_context_tokens_before
.map(|active_context_tokens_before| {
active_context_tokens_before
.saturating_sub(estimated_deleted_tokens.min(max_local_deleted_tokens))
});
}
let trace_input_history = compaction_trace
.is_enabled()
.then(|| history.raw_items().cloned().collect());
let prompt_input = history.for_prompt(&turn_context.model_info().input_modalities);
let tool_router = &step_context.tool_router;
let prompt = Prompt {
input: prompt_input,
tools: tool_router.model_visible_specs(),
parallel_tool_calls: true,
base_instructions,
output_schema: None,
output_schema_strict: true,
cyber_access_program: turn_context.cyber_access_program,
};
let responses_metadata = sess
.responses_metadata(
turn_context.as_ref(),
CodexResponsesRequestKind::Compaction(compaction_metadata),
)
.await;
let new_history = sess
.services
.model_client
.compact_conversation_history(
&prompt,
turn_context.model_info(),
turn_state,
CompactConversationRequestSettings {
effort: sess
.reasoning_effort_for_request(
&turn_context.initial_settings,
RequestEffortUsage::Compaction,
)
.await,
summary: turn_context.reasoning_summary(),
service_tier: if sess.services.auth_manager.auth_mode() == Some(AuthMode::ApiKey) {
None
} else {
step_context.settings.service_tier.clone()
},
},
&turn_context.session_telemetry,
compaction_trace,
&responses_metadata,
)
.await?;
Ok(RemoteCompactAttempt {
new_history,
trace_input_history,
})
}
+22 -10
View File
@@ -13,7 +13,6 @@ use crate::compact::compaction_status_from_result;
use crate::compact::insert_initial_context_before_last_real_user_or_summary;
use crate::compact_model_fallback::record_model_fallback;
use crate::compact_model_fallback::should_retry_with_current_model;
use crate::compact_remote::should_keep_compacted_history_item;
use crate::compact_remote_history::HistoryItemGroup;
use crate::compact_remote_history::history_item_groups;
use crate::context_manager::estimate_item_token_count;
@@ -72,8 +71,6 @@ enum RetainedImageBudget {
Enabled,
}
// Mirror the current /responses/compact retained-message default while the
// server-side path remains the reference implementation.
pub(crate) const RETAINED_MESSAGE_TOKEN_BUDGET: usize = 64_000;
const MAX_RETAINED_AGENT_MESSAGE_TOKENS: i64 = 10_000;
// Compact attempts can run much longer than normal turns, so keep the per-transport
@@ -502,11 +499,8 @@ fn build_v2_compacted_history(
.map(|(item, metadata)| ResponseItemEnvelope { item, metadata })
.collect::<Vec<_>>();
let retained = v2_history_item_groups(prompt_input)
.filter(|group| is_retained_for_remote_compaction_v2(&group.source.item))
.filter(|group| {
should_keep_compacted_history_item(&group.source.item)
|| (retain_client_developer_messages
&& is_client_authored_developer_message(&group.source))
is_retained_for_remote_compaction_v2(&group.source, retain_client_developer_messages)
})
.flat_map(HistoryItemGroup::into_items)
.collect::<Vec<_>>();
@@ -542,7 +536,11 @@ fn v2_history_item_groups(
})
}
fn is_retained_for_remote_compaction_v2(item: &ResponseItem) -> bool {
fn is_retained_for_remote_compaction_v2(
envelope: &ResponseItemEnvelope,
retain_client_developer_messages: bool,
) -> bool {
let item = &envelope.item;
if let ResponseItem::AgentMessage {
author,
recipient,
@@ -572,7 +570,16 @@ fn is_retained_for_remote_compaction_v2(item: &ResponseItem) -> bool {
return false;
};
matches!(role.as_str(), "user" | "developer" | "system")
match role.as_str() {
"user" => matches!(
crate::event_mapping::parse_turn_item(item),
Some(TurnItem::UserMessage(_) | TurnItem::HookPrompt(_))
),
"developer" => {
retain_client_developer_messages && is_client_authored_developer_message(envelope)
}
_ => false,
}
}
fn retained_input_image_count(item: &ResponseItem) -> usize {
@@ -824,10 +831,15 @@ mod tests {
#[test]
fn build_v2_compacted_history_filters_to_installed_retention_shape() {
let hook = codex_protocol::items::build_hook_prompt_message(&[
codex_protocol::items::HookPromptFragment::from_single_hook("hook", "hook-run"),
])
.expect("hook prompt");
let input = vec![
message("developer", "dev", /*phase*/ None),
message("system", "sys", /*phase*/ None),
message("user", "user", /*phase*/ None),
hook.clone(),
message("assistant", "commentary", Some(MessagePhase::Commentary)),
message("assistant", "final", Some(MessagePhase::FinalAnswer)),
ResponseItem::FunctionCall {
@@ -855,7 +867,7 @@ mod tests {
assert_eq!(
raw(history),
vec![message("user", "user", /*phase*/ None), output]
vec![message("user", "user", /*phase*/ None), hook, output]
);
}
@@ -5,7 +5,7 @@ use super::run_remote_compaction_request_v2;
use crate::Prompt;
use crate::client::ModelClientSession;
use crate::compact::CompactionAnalyticsDetails;
use crate::compact_remote::trim_function_call_history_to_fit_context_window;
use crate::compact_remote_history::trim_function_call_history_to_fit_context_window;
use crate::responses_metadata::CodexResponsesRequestKind;
use crate::responses_metadata::CompactionTurnMetadata;
use crate::session::session::Session;
-329
View File
@@ -6,40 +6,6 @@ use codex_protocol::models::ContentItemKind;
use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
use codex_protocol::models::InternalChatMessageMetadataPassthrough;
use pretty_assertions::assert_eq;
use std::sync::Arc;
async fn process_compacted_history_with_test_session(
compacted_history: Vec<ResponseItem>,
previous_turn_settings: Option<&PreviousTurnSettings>,
) -> (Vec<ResponseItem>, Vec<ResponseItem>) {
let (session, turn_context) = crate::session::tests::make_session_and_context().await;
let turn_context = Arc::new(turn_context);
session
.set_previous_turn_settings(previous_turn_settings.cloned())
.await;
let step_context =
crate::session::step_context::StepContext::for_test(Arc::clone(&turn_context));
let world_state = Arc::new(
session
.build_world_state_for_step(&step_context)
.await
.expect("world state should build"),
);
let initial_context = session
.build_initial_context_with_world_state(&step_context, world_state.as_ref())
.await;
let initial_context_injection = InitialContextInjection::BeforeLastUserMessage {
world_state,
step_context,
};
let (refreshed, _) = crate::compact_remote::process_compacted_history(
&session,
compacted_history,
&initial_context_injection,
)
.await;
(refreshed, initial_context)
}
fn annotated(items: Vec<ResponseItem>) -> Vec<ResponseItemEnvelope> {
items.into_iter().map(ResponseItemEnvelope::new).collect()
@@ -350,301 +316,6 @@ fn build_compacted_history_preserves_user_message_passthrough_metadata() {
);
}
#[tokio::test]
async fn process_compacted_history_replaces_developer_messages() {
let compacted_history = vec![
ResponseItem::Message {
id: None,
role: "developer".to_string(),
content: vec![ContentItem::InputText {
text: "stale permissions".to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "summary".to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
role: "developer".to_string(),
content: vec![ContentItem::InputText {
text: "stale personality".to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
},
];
let (refreshed, mut expected) = process_compacted_history_with_test_session(
compacted_history,
/*previous_turn_settings*/ None,
)
.await;
expected.push(ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "summary".to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
});
assert_eq!(refreshed, expected);
}
#[tokio::test]
async fn process_compacted_history_reinjects_full_initial_context() {
let compacted_history = vec![ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "summary".to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
}];
let (refreshed, mut expected) = process_compacted_history_with_test_session(
compacted_history,
/*previous_turn_settings*/ None,
)
.await;
expected.push(ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "summary".to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
});
assert_eq!(refreshed, expected);
}
#[tokio::test]
async fn process_compacted_history_drops_non_user_content_messages() {
let compacted_history = vec![
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: r#"# AGENTS.md instructions for /repo
<INSTRUCTIONS>
keep me updated
</INSTRUCTIONS>"#
.to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: r#"<environment_context>
<cwd>/repo</cwd>
<shell>zsh</shell>
</environment_context>"#
.to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: r#"<turn_aborted>
<turn_id>turn-1</turn_id>
<reason>interrupted</reason>
</turn_aborted>"#
.to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "summary".to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
role: "developer".to_string(),
content: vec![ContentItem::InputText {
text: "stale developer instructions".to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
},
];
let (refreshed, mut expected) = process_compacted_history_with_test_session(
compacted_history,
/*previous_turn_settings*/ None,
)
.await;
expected.push(ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "summary".to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
});
assert_eq!(refreshed, expected);
}
#[tokio::test]
async fn process_compacted_history_drops_legacy_warnings() {
let latest_user = user_message("latest user");
let compacted_history = vec![
user_message(
"Warning: The maximum number of unified exec processes you can keep open is 60 and you currently have 61 processes open. Reuse older processes or close them to prevent automatic pruning of old processes",
),
user_message(
"Warning: apply_patch was requested via exec_command. Use the apply_patch tool instead of exec_command.",
),
user_message(
"Warning: Your account was flagged for potentially high-risk cyber activity and this request was routed to gpt-5.2 as a fallback. To regain access to gpt-5.3-codex, apply for trusted access: https://chatgpt.com/cyber or learn more: https://developers.openai.com/codex/concepts/cyber-safety",
),
latest_user.clone(),
];
let (refreshed, initial_context) = process_compacted_history_with_test_session(
compacted_history,
/*previous_turn_settings*/ None,
)
.await;
let mut expected = initial_context;
expected.push(latest_user);
assert_eq!(refreshed, expected);
}
#[tokio::test]
async fn process_compacted_history_inserts_context_before_last_real_user_message_only() {
let compacted_history = vec![
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "older user".to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: format!("{SUMMARY_PREFIX}\nsummary text"),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "latest user".to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
},
];
let (refreshed, initial_context) = process_compacted_history_with_test_session(
compacted_history,
/*previous_turn_settings*/ None,
)
.await;
let mut expected = vec![
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "older user".to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
},
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: format!("{SUMMARY_PREFIX}\nsummary text"),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
},
];
expected.extend(initial_context);
expected.push(ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "latest user".to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
});
assert_eq!(refreshed, expected);
}
#[tokio::test]
async fn process_compacted_history_reinjects_model_switch_message() {
let compacted_history = vec![ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "summary".to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
}];
let previous_turn_settings = PreviousTurnSettings {
model: "previous-regular-model".to_string(),
comp_hash: None,
realtime_active: None,
};
let (refreshed, initial_context) = process_compacted_history_with_test_session(
compacted_history,
Some(&previous_turn_settings),
)
.await;
let ResponseItem::Message { role, content, .. } = &initial_context[0] else {
panic!("expected developer message");
};
assert_eq!(role, "developer");
let [ContentItem::InputText { text }, ..] = content.as_slice() else {
panic!("expected developer text");
};
assert!(text.contains("<model_switch>"));
let mut expected = initial_context;
expected.push(ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "summary".to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
});
assert_eq!(refreshed, expected);
}
#[test]
fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last() {
let agent_completion = ResponseItem::AgentMessage {
-5
View File
@@ -29,11 +29,6 @@ pub use responses_metadata::CodexResponsesMetadata;
pub use turn_metadata::detached_memory_responses_metadata;
mod codex_thread;
mod compact_model_fallback;
#[expect(
dead_code,
reason = "the unrouted v1 runner is removed in the follow-up"
)]
mod compact_remote;
mod compact_remote_history;
mod compact_remote_v2;
mod compact_token_budget;
+3 -4
View File
@@ -103,10 +103,9 @@ pub(crate) const MAX_EXTRA_METADATA_VALUE_BYTES: usize = 128;
/// Metadata attached to model requests whose purpose is conversation compaction.
///
/// This covers both local compaction requests sent through the normal `/responses` path and remote
/// compaction requests sent through `/responses/compact`. These fields describe the operation at
/// dispatch time. Post-response outcomes such as status, error, duration, and token deltas remain
/// in compaction analytics events.
/// This covers both local and remote compaction requests sent through the `/responses` path. These
/// fields describe the operation at dispatch time. Post-response outcomes such as status, error,
/// duration, and token deltas remain in compaction analytics events.
#[derive(Clone, Copy, Debug, Serialize)]
pub(crate) struct CompactionTurnMetadata {
trigger: CompactionTrigger,
-1
View File
@@ -11304,7 +11304,6 @@ async fn make_remote_compaction_session(
move |config| {
config.model = Some("gpt-5.2".to_string());
config.model_provider = provider;
let _ = config.features.enable(Feature::RemoteCompactionV2);
let _ = config.features.disable(Feature::TokenBudget);
},
)
-160
View File
@@ -1084,14 +1084,6 @@ fn base_mock() -> (MockBuilder, ResponseMock) {
(mock, response_mock)
}
fn compact_mock() -> (MockBuilder, ResponseMock) {
let response_mock = ResponseMock::new();
let mock = Mock::given(method("POST"))
.and(path_regex(".*/responses/compact$"))
.and(response_mock.clone());
(mock, response_mock)
}
fn models_mock() -> (MockBuilder, ModelsMock) {
let models_mock = ModelsMock::new();
let mock = Mock::given(method("GET"))
@@ -1122,119 +1114,6 @@ pub async fn mount_sse_once(server: &MockServer, body: String) -> ResponseMock {
response_mock
}
pub async fn mount_compact_json_once(server: &MockServer, body: serde_json::Value) -> ResponseMock {
mount_compact_response_once(
server,
ResponseTemplate::new(200)
.insert_header("content-type", "application/json")
.set_body_json(body),
)
.await
}
/// Mount a `/responses/compact` mock that mirrors the default remote compaction shape:
/// keep user+developer messages from the request, drop assistant/tool artifacts, and append one
/// compaction item carrying the provided summary text.
pub async fn mount_compact_user_history_with_summary_once(
server: &MockServer,
summary_text: &str,
) -> ResponseMock {
mount_compact_user_history_with_summary_sequence(server, vec![summary_text.to_string()]).await
}
/// Same as [`mount_compact_user_history_with_summary_once`], but for multiple compact calls.
/// Each incoming compact request receives the next summary text in order.
pub async fn mount_compact_user_history_with_summary_sequence(
server: &MockServer,
summary_texts: Vec<String>,
) -> ResponseMock {
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
#[derive(Debug)]
struct UserHistorySummaryResponder {
num_calls: AtomicUsize,
summary_texts: Vec<String>,
}
impl Respond for UserHistorySummaryResponder {
fn respond(&self, request: &wiremock::Request) -> ResponseTemplate {
let call_num = self.num_calls.fetch_add(1, Ordering::SeqCst);
let summary_text = self
.summary_texts
.get(call_num)
.expect("missing summary text for compact request");
let body_bytes = decode_body_bytes(
&request.body,
request
.headers
.get("content-encoding")
.and_then(|value| value.to_str().ok()),
);
let body_json: Value =
serde_json::from_slice(&body_bytes).expect("failed to parse compact request body");
let mut output = body_json
.get("input")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default()
.into_iter()
// TODO(ccunningham): Update this mock to match future compaction model behavior:
// return user/developer/assistant messages since the last compaction item, then
// append a single newest compaction item.
// Match current remote compaction behavior: keep user/developer messages and
// omit assistant/tool history entries.
.filter(|item| {
item.get("type").and_then(Value::as_str) == Some("message")
&& matches!(
item.get("role").and_then(Value::as_str),
Some("user") | Some("developer")
)
})
.collect::<Vec<Value>>();
let compaction_turn_id = body_json["client_metadata"]["turn_id"].as_str();
// Match Responses API: generated compaction items inherit the compact request turn.
let mut compaction_item = serde_json::json!({
"type": "compaction",
"encrypted_content": summary_text,
});
if let Some(turn_id) = compaction_turn_id {
compaction_item["internal_chat_message_metadata_passthrough"] =
serde_json::json!({ "turn_id": turn_id });
}
output.push(compaction_item);
ResponseTemplate::new(200)
.insert_header("content-type", "application/json")
.set_body_json(serde_json::json!({ "output": output }))
}
}
let num_calls = summary_texts.len();
let responder = UserHistorySummaryResponder {
num_calls: AtomicUsize::new(0),
summary_texts,
};
let (mock, response_mock) = compact_mock();
mock.respond_with(responder)
.up_to_n_times(num_calls as u64)
.expect(num_calls as u64)
.mount(server)
.await;
response_mock
}
pub async fn mount_compact_response_once(
server: &MockServer,
response: ResponseTemplate,
) -> ResponseMock {
let (mock, response_mock) = compact_mock();
mock.respond_with(response)
.up_to_n_times(1)
.mount(server)
.await;
response_mock
}
pub async fn mount_models_once(server: &MockServer, body: ModelsResponse) -> ModelsMock {
let (mock, models_mock) = models_mock();
mock.respond_with(
@@ -1622,45 +1501,6 @@ pub async fn mount_response_sequence(
response_mock
}
/// Mounts a sequence of responses for each POST to `/v1/responses/compact`.
/// Panics if more requests are received than responses provided.
pub async fn mount_compact_response_sequence(
server: &MockServer,
responses: Vec<ResponseTemplate>,
) -> ResponseMock {
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
struct SeqResponder {
num_calls: AtomicUsize,
responses: Vec<ResponseTemplate>,
}
impl Respond for SeqResponder {
fn respond(&self, _: &wiremock::Request) -> ResponseTemplate {
let call_num = self.num_calls.fetch_add(1, Ordering::SeqCst);
self.responses
.get(call_num)
.expect("missing response for compact call")
.clone()
}
}
let num_calls = responses.len();
let responder = SeqResponder {
num_calls: AtomicUsize::new(0),
responses,
};
let (mock, response_mock) = compact_mock();
mock.respond_with(responder)
.up_to_n_times(num_calls as u64)
.expect(num_calls as u64)
.mount(server)
.await;
response_mock
}
/// Validate invariants on the request body sent to `/v1/responses`.
///
/// - A `function_call_output` with missing/empty `call_id` must have a nonempty `name`.
-3
View File
@@ -637,9 +637,6 @@ async fn response_item_ids_are_sent_for_all_remote_v2_compaction_requests() -> a
.await;
let test = test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(|config| {
let _ = config.features.enable(Feature::RemoteCompactionV2);
})
.build(&server)
.await?;
+2 -9
View File
@@ -2892,7 +2892,6 @@ async fn pre_sampling_compact_falls_back_after_previous_model_invalid_request_on
config.model_provider = model_provider;
config.tool_registry.turn_metadata_includes_tool_info = true;
set_test_compact_prompt(config);
let _ = config.features.enable(Feature::RemoteCompactionV2);
});
let test = builder.build(&server).await.expect("build test codex");
@@ -5312,10 +5311,7 @@ async fn remote_v2_compaction_keeps_creation_time_instructions_after_same_path_m
)?;
let mut builder = test_codex()
.with_home(Arc::clone(&home))
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(|config| {
let _ = config.features.enable(Feature::RemoteCompactionV2);
});
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let test = builder.build(&server).await?;
// Materialize the old snapshot, rewrite the selected file in place, and compact remotely.
@@ -5375,10 +5371,7 @@ async fn remote_v2_compaction_keeps_creation_time_instructions_after_same_path_m
let mut resume_builder = test_codex()
.with_home(Arc::clone(&home))
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(move |config| {
config.cwd = resumed_cwd;
let _ = config.features.enable(Feature::RemoteCompactionV2);
});
.with_config(move |config| config.cwd = resumed_cwd);
let resumed = resume_builder
.resume(&server, Arc::clone(&home), rollout_path)
.await?;
+2 -22
View File
@@ -188,7 +188,6 @@ fn amazon_bedrock_test_codex() -> TestCodexBuilder {
.with_auth(auth)
.with_model(AMAZON_BEDROCK_GPT_5_5_MODEL_ID)
.with_config(|config| {
let _ = config.features.enable(Feature::RemoteCompactionV2);
config.model_provider = ModelProviderInfo {
base_url: config.model_provider.base_url.clone(),
..ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None)
@@ -353,10 +352,6 @@ async fn remote_compact_v2_retains_only_client_developer_messages_when_enabled(
test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(move |config| {
config
.features
.enable(Feature::RemoteCompactionV2)
.expect("remote compaction v2 should be configurable");
if enabled {
config
.features
@@ -441,14 +436,7 @@ async fn remote_compact_v2_records_usage_before_output_validation() -> Result<()
skip_if_no_network!(Ok(()));
let harness = TestCodexHarness::with_auto_env_builder(
test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(|config| {
config
.features
.enable(Feature::RemoteCompactionV2)
.expect("remote compaction v2 should be configurable");
}),
test_codex().with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()),
)
.await?;
let codex = &harness.test().codex;
@@ -588,7 +576,6 @@ async fn remote_compact_v2_charges_retained_images_to_token_budget(
test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(move |config| {
let _ = config.features.enable(Feature::RemoteCompactionV2);
let _ = config.features.enable(Feature::UnifiedImageBudget);
if let Some(enabled) = image_budget_enabled {
let _ = config
@@ -724,11 +711,7 @@ async fn remote_compact_v2_reuses_compaction_trigger_for_followups() -> Result<(
skip_if_no_network!(Ok(()));
let harness = TestCodexHarness::with_builder(
test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(|config| {
let _ = config.features.enable(Feature::RemoteCompactionV2);
}),
test_codex().with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()),
)
.await?;
let image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=";
@@ -1082,7 +1065,6 @@ async fn remote_compact_v2_retries_failures_with_stream_retry_budget() -> Result
.with_history_mode(ThreadHistoryMode::Paginated)
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(|config| {
let _ = config.features.enable(Feature::RemoteCompactionV2);
config.model_provider.request_max_retries = Some(0);
config.model_provider.stream_max_retries = Some(2);
}),
@@ -1418,7 +1400,6 @@ async fn remote_mid_turn_compact_v2_sends_turn_state_over_http() -> Result<()> {
test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(|config| {
let _ = config.features.enable(Feature::RemoteCompactionV2);
config.model_auto_compact_token_limit = Some(200);
}),
)
@@ -1547,7 +1528,6 @@ async fn remote_mid_turn_compact_v2_sends_turn_state_over_websocket() -> Result<
let mut builder = test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(|config| {
let _ = config.features.enable(Feature::RemoteCompactionV2);
config.model_auto_compact_token_limit = Some(200);
});
let test = builder.build_with_websocket_server(&server).await?;
@@ -217,10 +217,7 @@ async fn cyber_access_program_survives_mid_turn_remote_compaction_v2() -> Result
.await;
let test = test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(|config| {
let _ = config.features.disable(Feature::RemoteCompactionV2);
config.model_auto_compact_token_limit = Some(200);
})
.with_config(|config| config.model_auto_compact_token_limit = Some(200))
.build_with_auto_env(&server)
.await?;
@@ -1255,7 +1255,6 @@ async fn terminal_compaction_error_does_not_retry_pending_input(
.with_config(move |config| {
config.model_provider.base_url = Some(base_url);
config.model_auto_compact_token_limit = Some(100_000);
let _ = config.features.enable(Feature::RemoteCompactionV2);
// The streaming fixture records raw request bodies for JSON assertions.
let _ = config.features.disable(Feature::EnableRequestCompression);
})
+1 -6
View File
@@ -376,12 +376,7 @@ async fn compaction_budget_exhaustion_fails_without_retry(
reminder_at_remaining_tokens: vec![5],
..rollout_budget()
});
if remote_v2 {
config
.features
.enable(Feature::RemoteCompactionV2)
.expect("test config should allow remote compaction v2");
} else {
if !remote_v2 {
config.model_provider.name = "OpenAI-compatible test provider".to_string();
}
})
@@ -1,50 +0,0 @@
---
source: core/tests/suite/compact_remote.rs
expression: "context_snapshot::format_request_body_diff_snapshot(scenario,\n\"Last Normal /responses Request\", &normal_request,\n\"Remote /responses/compact Request\", &compact_request,\n&ContextSnapshotOptions::default(),)"
---
Scenario: After five varied API-key-auth turns, remote manual compaction omits service_tier, reuses prompt_cache_key, and still omits responses-only fields.
--- Last Normal /responses Request
+++ Remote /responses/compact Request
- "client_metadata": {
- "root_turn_id": "<UUID>",
- "session_id": "<UUID>",
- "thread_id": "<UUID>",
- "turn_id": "<UUID>",
- "x-codex-installation-id": "<UUID>",
- "x-codex-turn-metadata": "{\"installation_id\":\"<UUID>\",\"session_id\":\"<UUID>\",\"thread_id\":\"<UUID>\",\"agent_name\":\"/root\",\"turn_id\":\"<UUID>\",\"window_id\":\"<UUID>:0\",\"window_number\":0,\"context_window_id\":\"<UUID>\",\"request_kind\":\"turn\",\"root_turn_id\":\"<UUID>\",\"sandbox\":\"<SANDBOX>\",\"sandbox_mode\":\"danger-full-access\",\"auto_review_enabled\":false,\"node_repl_auto_review_required\":false,\"node_repl_disabled\":false,\"turn_started_at_unix_ms\":<UNIX_MS>}",
- "x-codex-window-id": "<UUID>:0"
- },
- "include": [
- "reasoning.encrypted_content"
- ],
+ },
+ {
+ "content": [
+ {
+ "text": "turn five raw content",
+ "type": "reasoning_text"
+ }
+ ],
+ "encrypted_content": "YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYnR1cm4gZml2ZSByYXcgY29udGVudA==",
+ "summary": [
+ {
+ "text": "TURN_FIVE_REASONING",
+ "type": "summary_text"
+ }
+ ],
+ "type": "reasoning"
+ },
+ {
+ "content": [
+ {
+ "text": "TURN_FIVE_ASSISTANT",
+ "type": "output_text"
+ }
+ ],
+ "role": "assistant",
+ "type": "message"
- "service_tier": "priority",
- "store": false,
- "stream": true,
- "tool_choice": "auto",
@@ -1,49 +0,0 @@
---
source: core/tests/suite/compact_remote.rs
expression: "context_snapshot::format_request_body_diff_snapshot(scenario,\n\"Last Normal /responses Request\", &normal_request,\n\"Remote /responses/compact Request\", &compact_request,\n&ContextSnapshotOptions::default(),)"
---
Scenario: After five varied ChatGPT-auth turns, remote manual compaction reuses service_tier and prompt_cache_key while omitting responses-only fields.
--- Last Normal /responses Request
+++ Remote /responses/compact Request
- "client_metadata": {
- "root_turn_id": "<UUID>",
- "session_id": "<UUID>",
- "thread_id": "<UUID>",
- "turn_id": "<UUID>",
- "x-codex-installation-id": "<UUID>",
- "x-codex-turn-metadata": "{\"installation_id\":\"<UUID>\",\"session_id\":\"<UUID>\",\"thread_id\":\"<UUID>\",\"agent_name\":\"/root\",\"turn_id\":\"<UUID>\",\"window_id\":\"<UUID>:0\",\"window_number\":0,\"context_window_id\":\"<UUID>\",\"request_kind\":\"turn\",\"root_turn_id\":\"<UUID>\",\"sandbox\":\"<SANDBOX>\",\"sandbox_mode\":\"danger-full-access\",\"auto_review_enabled\":false,\"node_repl_auto_review_required\":false,\"node_repl_disabled\":false,\"turn_started_at_unix_ms\":<UNIX_MS>}",
- "x-codex-window-id": "<UUID>:0"
- },
- "include": [
- "reasoning.encrypted_content"
- ],
+ },
+ {
+ "content": [
+ {
+ "text": "turn five raw content",
+ "type": "reasoning_text"
+ }
+ ],
+ "encrypted_content": "YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYnR1cm4gZml2ZSByYXcgY29udGVudA==",
+ "summary": [
+ {
+ "text": "TURN_FIVE_REASONING",
+ "type": "summary_text"
+ }
+ ],
+ "type": "reasoning"
+ },
+ {
+ "content": [
+ {
+ "text": "TURN_FIVE_ASSISTANT",
+ "type": "output_text"
+ }
+ ],
+ "role": "assistant",
+ "type": "message"
- "store": false,
- "stream": true,
- "tool_choice": "auto",
@@ -1,21 +0,0 @@
---
source: core/tests/suite/compact_remote.rs
expression: "format_labeled_requests_snapshot(\"Remote manual /compact while realtime remains active: the next regular turn restates realtime-start instructions after compaction clears the baseline.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", post_compact_request),])"
---
Scenario: Remote manual /compact while realtime remains active: the next regular turn restates realtime-start instructions after compaction clears the baseline.
## Remote Compaction Request
00:message/developer[2]:
[01] <realtime_conversation>\nRealtime conversation started.\n\nYou a...
[02] <PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:USER_ONE
03:message/assistant:REMOTE_FIRST_REPLY
## Remote Post-Compaction History Layout
00:compaction:encrypted=true
01:message/developer[2]:
[01] <realtime_conversation>\nRealtime conversation started.\n\nYou a...
[02] <PERMISSIONS_INSTRUCTIONS>
02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
03:message/user:USER_TWO
@@ -1,17 +0,0 @@
---
source: core/tests/suite/compact_remote.rs
expression: "format_labeled_requests_snapshot(\"Remote manual /compact where remote compact output is compaction-only: follow-up layout uses the returned compaction item plus new user message.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", follow_up_request),])"
---
Scenario: Remote manual /compact where remote compact output is compaction-only: follow-up layout uses the returned compaction item plus new user message.
## Remote Compaction Request
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:hello remote compact
03:message/assistant:FIRST_REMOTE_REPLY
## Remote Post-Compaction History Layout
00:compaction:encrypted=true
01:message/developer:<PERMISSIONS_INSTRUCTIONS>
02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
03:message/user:after compact
@@ -1,10 +0,0 @@
---
source: core/tests/suite/compact_remote.rs
expression: "format_labeled_requests_snapshot(\"Remote manual /compact with no prior user turn skips the remote compact request; the follow-up turn carries canonical context and new user message.\",\n&[(\"Remote Post-Compaction History Layout\", &follow_up_request)])"
---
Scenario: Remote manual /compact with no prior user turn skips the remote compact request; the follow-up turn carries canonical context and new user message.
## Remote Post-Compaction History Layout
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:USER_ONE
@@ -1,32 +0,0 @@
---
source: core/tests/suite/compact_remote.rs
expression: "format_labeled_requests_snapshot(\"Remote mid-turn continuation compaction after realtime was closed before the turn: the initial second-turn request emits realtime-end instructions, but the continuation request does not restate them after compaction because the current turn already established the inactive baseline.\",\n&[(\"Second Turn Initial Request\", second_turn_request),\n(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", post_compact_request),])"
---
Scenario: Remote mid-turn continuation compaction after realtime was closed before the turn: the initial second-turn request emits realtime-end instructions, but the continuation request does not restate them after compaction because the current turn already established the inactive baseline.
## Second Turn Initial Request
00:message/developer[2]:
[01] <realtime_conversation>\nRealtime conversation started.\n\nYou a...
[02] <PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:SETUP_USER
03:message/assistant:REMOTE_SETUP_REPLY
04:message/developer:<realtime_conversation>\nRealtime conversation ended.\n\nSubsequ...
05:message/user:USER_TWO
## Remote Compaction Request
00:message/developer[2]:
[01] <realtime_conversation>\nRealtime conversation started.\n\nYou a...
[02] <PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:SETUP_USER
03:message/assistant:REMOTE_SETUP_REPLY
04:message/developer:<realtime_conversation>\nRealtime conversation ended.\n\nSubsequ...
05:message/user:USER_TWO
06:function_call/test_tool
07:function_call_output:unsupported call: test_tool
## Remote Post-Compaction History Layout
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:compaction:encrypted=true
@@ -1,16 +0,0 @@
---
source: core/tests/suite/compact_remote.rs
expression: "format_labeled_requests_snapshot(\"After a prior manual /compact produced an older remote compaction item, the next turn hits remote auto-compaction before the next sampling request. The compact request carries forward that earlier compaction item, and the next sampling request shows the latest compaction item with context reinjected before USER_TWO.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Second Turn Request (After Compaction)\", &second_turn_request),])"
---
Scenario: After a prior manual /compact produced an older remote compaction item, the next turn hits remote auto-compaction before the next sampling request. The compact request carries forward that earlier compaction item, and the next sampling request shows the latest compaction item with context reinjected before USER_TWO.
## Remote Compaction Request
00:message/user:USER_ONE
01:compaction:encrypted=true
## Second Turn Request (After Compaction)
00:message/user:USER_ONE
01:compaction:encrypted=true
02:message/developer:<PERMISSIONS_INSTRUCTIONS>
03:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
04:message/user:USER_TWO
@@ -1,18 +0,0 @@
---
source: core/tests/suite/compact_remote.rs
expression: "format_labeled_requests_snapshot(\"Remote mid-turn continuation compaction after tool output: compact request includes tool artifacts and the follow-up request includes the returned compaction item.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &requests[1]),])"
---
Scenario: Remote mid-turn continuation compaction after tool output: compact request includes tool artifacts and the follow-up request includes the returned compaction item.
## Remote Compaction Request
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:USER_ONE
03:function_call/test_tool
04:function_call_output:unsupported call: test_tool
## Remote Post-Compaction History Layout
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:USER_ONE
03:compaction:encrypted=true
@@ -1,17 +0,0 @@
---
source: core/tests/suite/compact_remote.rs
expression: "format_labeled_requests_snapshot(\"Remote mid-turn compaction where compact output has only a compaction item: continuation layout reinjects context before that compaction item.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &post_compact_turn_request),])"
---
Scenario: Remote mid-turn compaction where compact output has only a compaction item: continuation layout reinjects context before that compaction item.
## Remote Compaction Request
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:USER_ONE
03:function_call/test_tool
04:function_call_output:unsupported call: test_tool
## Remote Post-Compaction History Layout
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:compaction:encrypted=true
@@ -1,11 +0,0 @@
---
source: core/tests/suite/compact_remote.rs
expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction context-window failure: compaction request excludes the incoming user message and the turn errors.\",\n&[(\"Remote Compaction Request (Incoming User Excluded)\",\n&include_attempt_request),])"
---
Scenario: Remote pre-turn auto-compaction context-window failure: compaction request excludes the incoming user message and the turn errors.
## Remote Compaction Request (Incoming User Excluded)
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:USER_ONE
03:message/assistant:REMOTE_FIRST_REPLY
@@ -1,11 +0,0 @@
---
source: core/tests/suite/compact_remote.rs
expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction parse failure: compaction request excludes the incoming user message and the turn stops.\",\n&[(\"Remote Compaction Request (Incoming User Excluded)\",\n&first_compact_mock.single_request()),])"
---
Scenario: Remote pre-turn auto-compaction parse failure: compaction request excludes the incoming user message and the turn stops.
## Remote Compaction Request (Incoming User Excluded)
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:turn that exceeds token threshold
03:message/assistant:initial turn complete
@@ -1,21 +0,0 @@
---
source: core/tests/suite/compact_remote.rs
expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction with a context override emits the context diff in the compact request while excluding the incoming user message.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &requests[2]),])"
---
Scenario: Remote pre-turn auto-compaction with a context override emits the context diff in the compact request while excluding the incoming user message.
## Remote Compaction Request
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:USER_ONE
03:message/assistant:REMOTE_FIRST_REPLY
04:message/user:USER_TWO
05:message/assistant:REMOTE_SECOND_REPLY
## Remote Post-Compaction History Layout
00:message/user:USER_ONE
01:message/user:USER_TWO
02:compaction:encrypted=true
03:message/developer:<PERMISSIONS_INSTRUCTIONS>
04:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
05:message/user:USER_THREE
@@ -1,21 +0,0 @@
---
source: core/tests/suite/compact_remote.rs
expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction while realtime remains active: compaction clears the reference baseline, so the follow-up request restates realtime-start instructions.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", post_compact_request),])"
---
Scenario: Remote pre-turn auto-compaction while realtime remains active: compaction clears the reference baseline, so the follow-up request restates realtime-start instructions.
## Remote Compaction Request
00:message/developer[2]:
[01] <realtime_conversation>\nRealtime conversation started.\n\nYou a...
[02] <PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:USER_ONE
03:message/assistant:REMOTE_FIRST_REPLY
## Remote Post-Compaction History Layout
00:compaction:encrypted=true
01:message/developer[2]:
[01] <realtime_conversation>\nRealtime conversation started.\n\nYou a...
[02] <PERMISSIONS_INSTRUCTIONS>
02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
03:message/user:USER_TWO
@@ -1,25 +0,0 @@
---
source: core/tests/suite/compact_remote.rs
expression: "format_labeled_requests_snapshot(\"Remote pre-turn compaction during model switch currently excludes incoming user input, strips incoming <model_switch> from the compact request payload, and restores it in the post-compaction follow-up request.\",\n&[(\"Initial Request (Previous Model)\", &initial_turn_request),\n(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &post_compact_turn_request),])"
---
Scenario: Remote pre-turn compaction during model switch currently excludes incoming user input, strips incoming <model_switch> from the compact request payload, and restores it in the post-compaction follow-up request.
## Initial Request (Previous Model)
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:BEFORE_SWITCH_USER
## Remote Compaction Request
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:BEFORE_SWITCH_USER
03:message/assistant:BEFORE_SWITCH_REPLY
## Remote Post-Compaction History Layout
00:message/user:BEFORE_SWITCH_USER
01:compaction:encrypted=true
02:message/developer[2]:
[01] <model_switch>\nThe user was previously using a different model....
[02] <PERMISSIONS_INSTRUCTIONS>
03:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
04:message/user:AFTER_SWITCH_USER
+1 -1
View File
@@ -346,7 +346,7 @@ pub enum Feature {
RealtimeConversation,
/// Prevent idle system sleep while a turn is actively running.
PreventIdleSleep,
/// Enable remote compaction v2 over the normal Responses API.
/// Removed compatibility key, still advertised to the Responses API.
RemoteCompactionV2,
/// Include retained images in the remote compaction context budget.
CompactionImageBudget,
-2
View File
@@ -162,8 +162,6 @@ def assistant_message_with_phase(
def request_kind(request_path: str) -> str:
"""Classify captured mock-server request paths for compact assertions."""
if request_path.endswith("/responses/compact"):
return "compact"
if request_path.endswith("/responses"):
return "responses"
return request_path