Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion codex-rs/core/src/client_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,15 @@ impl Default for Prompt {

impl Prompt {
pub(crate) fn get_formatted_input(&self) -> Vec<ResponseItem> {
self.input
Self::format_input(&self.input)
}

/// Converts persisted assistant envelopes into the model-native input items
/// used by the Responses API. Callers preparing history for retention must
/// use the same conversion as the request builder so encrypted delegated
/// messages are not filtered as ordinary assistant text.
pub(crate) fn format_input(input: &[ResponseItem]) -> Vec<ResponseItem> {
input
.iter()
.cloned()
.map(|item| {
Expand Down
49 changes: 45 additions & 4 deletions codex-rs/core/src/compact_remote_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::compact_remote::log_remote_compact_failure;
use crate::compact_remote::process_compacted_history;
use crate::compact_remote::should_keep_compacted_history_item;
use crate::compact_remote::trim_function_call_history_to_fit_context_window;
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;
Expand Down Expand Up @@ -55,6 +56,7 @@ use tracing::info;
// Mirror the current /responses/compact retained-message default while the
// server-side path remains the reference implementation.
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
// retry budget smaller than the general Responses stream retry budget.
const MAX_REMOTE_COMPACTION_V2_STREAM_RETRIES: u64 = 2;
Expand Down Expand Up @@ -392,7 +394,11 @@ async fn run_remote_compact_v2_attempt(
let prompt_input = history
.clone()
.for_prompt(&turn_context.model_info.input_modalities);
let mut input = prompt_input.clone();
// Apply the same persisted-envelope conversion used by the Responses request
// builder before retention. Otherwise encrypted delegated messages are still
// assistant envelopes here and the retention role filter drops them.
let formatted_prompt_input = Prompt::format_input(&prompt_input);
let mut input = formatted_prompt_input.clone();
input.push(ResponseItem::CompactionTrigger);
let prompt = Prompt {
input,
Expand Down Expand Up @@ -450,7 +456,7 @@ async fn run_remote_compact_v2_attempt(
.await;
Ok(RemoteCompactV2Attempt {
trace_input_history,
prompt_input,
prompt_input: formatted_prompt_input,
compaction_output,
token_usage,
owned_client_session,
Expand Down Expand Up @@ -616,6 +622,10 @@ fn build_v2_compacted_history(
}

fn is_retained_for_remote_compaction_v2(item: &ResponseItem) -> bool {
if matches!(item, ResponseItem::AgentMessage { .. }) {
return estimate_item_token_count(item) <= MAX_RETAINED_AGENT_MESSAGE_TOKENS;
}

let ResponseItem::Message { role, .. } = item else {
return false;
};
Expand Down Expand Up @@ -651,7 +661,7 @@ fn truncate_retained_messages_for_remote_compaction(

fn message_text_token_count(item: &ResponseItem) -> usize {
let ResponseItem::Message { content, .. } = item else {
return 0;
return usize::try_from(estimate_item_token_count(item)).unwrap_or(usize::MAX);
};

content
Expand All @@ -676,7 +686,7 @@ fn truncate_message_text_to_token_budget(
phase,
} = item
else {
return Some(item);
return None;
};

let mut remaining = max_tokens;
Expand Down Expand Up @@ -718,8 +728,10 @@ fn truncate_message_text_to_token_budget(
#[cfg(test)]
mod tests {
use super::*;
use codex_protocol::AgentPath;
use codex_protocol::models::ContentItem;
use codex_protocol::models::MessagePhase;
use codex_protocol::protocol::InterAgentCommunication;
use pretty_assertions::assert_eq;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
Expand Down Expand Up @@ -780,6 +792,35 @@ mod tests {
);
}

#[test]
fn build_v2_compacted_history_retains_encrypted_delegated_message_after_formatting() {
let communication = InterAgentCommunication::new_encrypted(
AgentPath::root(),
AgentPath::root().join("worker").expect("valid worker path"),
Vec::new(),
"encrypted delegated task".to_string(),
/*trigger_turn*/ true,
);
let persisted_envelope: ResponseItem = communication.to_response_input_item().into();
assert!(matches!(
persisted_envelope,
ResponseItem::Message { ref role, .. } if role == "assistant"
));

let formatted = Prompt::format_input(&[persisted_envelope]);
assert!(matches!(
formatted.first(),
Some(ResponseItem::AgentMessage { .. })
));

let output = ResponseItem::Compaction {
encrypted_content: "new".to_string(),
};
let history = build_v2_compacted_history(&formatted, output.clone());

assert_eq!(history, vec![formatted[0].clone(), output]);
}

#[test]
fn build_v2_compacted_history_discards_messages_before_truncating() {
let old = message("user", "old", /*phase*/ None);
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/core/src/context_manager/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,7 @@ fn estimate_encrypted_function_output_length(encoded_len: usize) -> usize {
encoded_len.saturating_mul(9).div_ceil(16)
}

fn estimate_item_token_count(item: &ResponseItem) -> i64 {
pub(crate) fn estimate_item_token_count(item: &ResponseItem) -> i64 {
let model_visible_bytes = estimate_response_item_model_visible_bytes(item);
approx_tokens_from_byte_count_i64(model_visible_bytes)
}
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/context_manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub(crate) mod updates;

pub(crate) use history::ContextManager;
pub(crate) use history::TotalTokenUsageBreakdown;
pub(crate) use history::estimate_item_token_count;
pub(crate) use history::estimate_response_item_model_visible_bytes;
pub(crate) use history::is_user_turn_boundary;
pub(crate) use history::truncate_function_output_payload;
Loading