mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Filter legacy warning messages during compaction (#22243)
## Why Older sessions can contain model-warning records persisted as `user` messages, including the unified exec process-limit warning, the `apply_patch`-via-`exec_command` warning, and the model-mismatch high-risk cyber fallback warning. Those warnings are no longer produced as conversation history items, but when old sessions compact they should still be recognized as injected context rather than preserved as real user turns. ## What changed - Removed `record_model_warning` and the production paths that emitted these warning messages into conversation history. - Added `LegacyUnifiedExecProcessLimitWarning`, `LegacyApplyPatchExecCommandWarning`, and `LegacyModelMismatchWarning` contextual fragments that are used only for matching old persisted messages. - Registered the legacy fragments with contextual user message detection so compaction filters them through the existing fragment path. - Added focused compaction coverage for old warning messages being dropped during compacted-history processing. ## Testing - `cargo test -p codex-core warning` - `just fix -p codex-core`
This commit is contained in:
committed by
GitHub
Unverified
parent
d08906a944
commit
79c65f816c
@@ -287,8 +287,9 @@ pub(crate) async fn process_compacted_history(
|
||||
///
|
||||
/// This intentionally keeps:
|
||||
/// - `assistant` messages (future remote compaction models may emit them)
|
||||
/// - `user`-role warnings and compaction-generated summary messages because
|
||||
/// they parse as `TurnItem::UserMessage`.
|
||||
/// - `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.
|
||||
fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
|
||||
match item {
|
||||
ResponseItem::Message { role, .. } if role == "developer" => false,
|
||||
|
||||
@@ -23,6 +23,17 @@ async fn process_compacted_history_with_test_session(
|
||||
(refreshed, initial_context)
|
||||
}
|
||||
|
||||
fn user_message(text: &str) -> ResponseItem {
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: text.to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content_items_to_text_joins_non_empty_segments() {
|
||||
let items = vec![
|
||||
@@ -120,6 +131,26 @@ do things
|
||||
assert_eq!(vec!["real user message".to_string()], collected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_user_messages_filters_legacy_warnings() {
|
||||
let items = 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",
|
||||
),
|
||||
user_message("real user message"),
|
||||
];
|
||||
|
||||
let collected = collect_user_messages(&items);
|
||||
|
||||
assert_eq!(vec!["real user message".to_string()], collected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_token_limited_compacted_history_truncates_overlong_user_messages() {
|
||||
// Use a small truncation limit so the test remains fast while still validating
|
||||
@@ -351,6 +382,31 @@ keep me updated
|
||||
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![
|
||||
|
||||
@@ -6,6 +6,9 @@ use super::EnvironmentContext;
|
||||
use super::FragmentRegistration;
|
||||
use super::FragmentRegistrationProxy;
|
||||
use super::GoalContext;
|
||||
use super::LegacyApplyPatchExecCommandWarning;
|
||||
use super::LegacyModelMismatchWarning;
|
||||
use super::LegacyUnifiedExecProcessLimitWarning;
|
||||
use super::SkillInstructions;
|
||||
use super::SubagentNotification;
|
||||
use super::TurnAborted;
|
||||
@@ -26,6 +29,15 @@ static SUBAGENT_NOTIFICATION_REGISTRATION: FragmentRegistrationProxy<SubagentNot
|
||||
FragmentRegistrationProxy::new();
|
||||
static GOAL_CONTEXT_REGISTRATION: FragmentRegistrationProxy<GoalContext> =
|
||||
FragmentRegistrationProxy::new();
|
||||
static LEGACY_UNIFIED_EXEC_PROCESS_LIMIT_WARNING_REGISTRATION: FragmentRegistrationProxy<
|
||||
LegacyUnifiedExecProcessLimitWarning,
|
||||
> = FragmentRegistrationProxy::new();
|
||||
static LEGACY_APPLY_PATCH_EXEC_COMMAND_WARNING_REGISTRATION: FragmentRegistrationProxy<
|
||||
LegacyApplyPatchExecCommandWarning,
|
||||
> = FragmentRegistrationProxy::new();
|
||||
static LEGACY_MODEL_MISMATCH_WARNING_REGISTRATION: FragmentRegistrationProxy<
|
||||
LegacyModelMismatchWarning,
|
||||
> = FragmentRegistrationProxy::new();
|
||||
|
||||
static CONTEXTUAL_USER_FRAGMENTS: &[&dyn FragmentRegistration] = &[
|
||||
&USER_INSTRUCTIONS_REGISTRATION,
|
||||
@@ -35,6 +47,9 @@ static CONTEXTUAL_USER_FRAGMENTS: &[&dyn FragmentRegistration] = &[
|
||||
&TURN_ABORTED_REGISTRATION,
|
||||
&SUBAGENT_NOTIFICATION_REGISTRATION,
|
||||
&GOAL_CONTEXT_REGISTRATION,
|
||||
&LEGACY_UNIFIED_EXEC_PROCESS_LIMIT_WARNING_REGISTRATION,
|
||||
&LEGACY_APPLY_PATCH_EXEC_COMMAND_WARNING_REGISTRATION,
|
||||
&LEGACY_MODEL_MISMATCH_WARNING_REGISTRATION,
|
||||
];
|
||||
|
||||
fn is_standard_contextual_user_text(text: &str) -> bool {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
use super::ContextualUserFragment;
|
||||
|
||||
// This warning is not produced anymore but fragment definition is used to filter messaged from old sessions
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct LegacyApplyPatchExecCommandWarning;
|
||||
|
||||
impl ContextualUserFragment for LegacyApplyPatchExecCommandWarning {
|
||||
const ROLE: &'static str = "user";
|
||||
const START_MARKER: &'static str = "";
|
||||
const END_MARKER: &'static str = "";
|
||||
|
||||
fn matches_text(text: &str) -> bool {
|
||||
let trimmed = text.trim();
|
||||
trimmed.starts_with("Warning: apply_patch was requested via ")
|
||||
&& trimmed.ends_with("Use the apply_patch tool instead of exec_command.")
|
||||
}
|
||||
|
||||
fn body(&self) -> String {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use super::ContextualUserFragment;
|
||||
|
||||
// This warning is not produced anymore but fragment definition is used to filter messaged from old sessions
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct LegacyModelMismatchWarning;
|
||||
|
||||
impl ContextualUserFragment for LegacyModelMismatchWarning {
|
||||
const ROLE: &'static str = "user";
|
||||
const START_MARKER: &'static str = "";
|
||||
const END_MARKER: &'static str = "";
|
||||
|
||||
fn matches_text(text: &str) -> bool {
|
||||
text.trim().starts_with(
|
||||
"Warning: Your account was flagged for potentially high-risk cyber activity",
|
||||
)
|
||||
}
|
||||
|
||||
fn body(&self) -> String {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use super::ContextualUserFragment;
|
||||
|
||||
// This warning is not produced anymore but fragment definition is used to filter messaged from old sessions
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct LegacyUnifiedExecProcessLimitWarning;
|
||||
|
||||
impl ContextualUserFragment for LegacyUnifiedExecProcessLimitWarning {
|
||||
const ROLE: &'static str = "user";
|
||||
const START_MARKER: &'static str = "";
|
||||
const END_MARKER: &'static str = "";
|
||||
|
||||
fn matches_text(text: &str) -> bool {
|
||||
text.trim().starts_with(
|
||||
"Warning: The maximum number of unified exec processes you can keep open is",
|
||||
)
|
||||
}
|
||||
|
||||
fn body(&self) -> String {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,9 @@ mod goal_context;
|
||||
mod guardian_followup_review_reminder;
|
||||
mod hook_additional_context;
|
||||
mod image_generation_instructions;
|
||||
mod legacy_apply_patch_exec_command_warning;
|
||||
mod legacy_model_mismatch_warning;
|
||||
mod legacy_unified_exec_process_limit_warning;
|
||||
mod model_switch_instructions;
|
||||
mod network_rule_saved;
|
||||
mod permissions_instructions;
|
||||
@@ -41,6 +44,9 @@ pub(crate) use goal_context::GoalContext;
|
||||
pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder;
|
||||
pub(crate) use hook_additional_context::HookAdditionalContext;
|
||||
pub(crate) use image_generation_instructions::ImageGenerationInstructions;
|
||||
pub(crate) use legacy_apply_patch_exec_command_warning::LegacyApplyPatchExecCommandWarning;
|
||||
pub(crate) use legacy_model_mismatch_warning::LegacyModelMismatchWarning;
|
||||
pub(crate) use legacy_unified_exec_process_limit_warning::LegacyUnifiedExecProcessLimitWarning;
|
||||
pub(crate) use model_switch_instructions::ModelSwitchInstructions;
|
||||
pub(crate) use network_rule_saved::NetworkRuleSaved;
|
||||
pub use permissions_instructions::PermissionsInstructions;
|
||||
|
||||
@@ -2436,22 +2436,6 @@ impl Session {
|
||||
state.record_items(items.iter(), turn_context.truncation_policy);
|
||||
}
|
||||
|
||||
pub(crate) async fn record_model_warning(&self, message: impl Into<String>, ctx: &TurnContext) {
|
||||
self.services
|
||||
.session_telemetry
|
||||
.counter("codex.model_warning", /*inc*/ 1, &[]);
|
||||
let item = ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: format!("Warning: {}", message.into()),
|
||||
}],
|
||||
phase: None,
|
||||
};
|
||||
|
||||
self.record_conversation_items(ctx, &[item]).await;
|
||||
}
|
||||
|
||||
async fn maybe_warn_on_server_model_mismatch(
|
||||
self: &Arc<Self>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
@@ -2488,8 +2472,6 @@ impl Session {
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
self.record_model_warning(warning_message, turn_context)
|
||||
.await;
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -5797,34 +5797,6 @@ async fn refresh_mcp_servers_is_deferred_until_next_turn() {
|
||||
assert!(!new_token.is_cancelled());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn record_model_warning_appends_user_message() {
|
||||
let (mut session, turn_context) = make_session_and_context().await;
|
||||
let features = Features::with_defaults().into();
|
||||
session.features = features;
|
||||
|
||||
session
|
||||
.record_model_warning("too many unified exec processes", &turn_context)
|
||||
.await;
|
||||
|
||||
let history = session.clone_history().await;
|
||||
let history_items = history.raw_items();
|
||||
let last = history_items.last().expect("warning recorded");
|
||||
|
||||
match last {
|
||||
ResponseItem::Message { role, content, .. } => {
|
||||
assert_eq!(role, "user");
|
||||
assert_eq!(
|
||||
content,
|
||||
&vec![ContentItem::InputText {
|
||||
text: "Warning: too many unified exec processes".to_string(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
other => panic!("expected user message, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn spawn_task_does_not_update_previous_turn_settings_for_non_run_turn_tasks() {
|
||||
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
|
||||
|
||||
@@ -506,14 +506,6 @@ pub(crate) async fn intercept_apply_patch(
|
||||
.await
|
||||
{
|
||||
codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => {
|
||||
session
|
||||
.record_model_warning(
|
||||
format!(
|
||||
"apply_patch was requested via {tool_name}. Use the apply_patch tool instead of exec_command."
|
||||
),
|
||||
turn.as_ref(),
|
||||
)
|
||||
.await;
|
||||
let (approval_keys, effective_additional_permissions, file_system_sandbox_policy) =
|
||||
effective_patch_permissions(session.as_ref(), turn.as_ref(), &changes, cwd).await;
|
||||
match apply_patch::apply_patch(turn.as_ref(), &file_system_sandbox_policy, changes)
|
||||
|
||||
@@ -68,9 +68,6 @@ pub(crate) const UNIFIED_EXEC_OUTPUT_MAX_BYTES: usize = 1024 * 1024; // 1 MiB
|
||||
pub(crate) const UNIFIED_EXEC_OUTPUT_MAX_TOKENS: usize = UNIFIED_EXEC_OUTPUT_MAX_BYTES / 4;
|
||||
pub(crate) const MAX_UNIFIED_EXEC_PROCESSES: usize = 64;
|
||||
|
||||
// Send a warning message to the models when it reaches this number of processes.
|
||||
pub(crate) const WARNING_UNIFIED_EXEC_PROCESSES: usize = 60;
|
||||
|
||||
pub(crate) struct UnifiedExecContext {
|
||||
pub session: Arc<Session>,
|
||||
pub turn: Arc<TurnContext>,
|
||||
|
||||
@@ -37,7 +37,6 @@ use crate::unified_exec::ProcessStore;
|
||||
use crate::unified_exec::UnifiedExecContext;
|
||||
use crate::unified_exec::UnifiedExecError;
|
||||
use crate::unified_exec::UnifiedExecProcessManager;
|
||||
use crate::unified_exec::WARNING_UNIFIED_EXEC_PROCESSES;
|
||||
use crate::unified_exec::WriteStdinRequest;
|
||||
use crate::unified_exec::async_watcher::emit_exec_end_for_unified_exec;
|
||||
use crate::unified_exec::async_watcher::emit_failed_exec_end_for_unified_exec;
|
||||
@@ -827,11 +826,11 @@ impl UnifiedExecProcessManager {
|
||||
session: Arc::downgrade(&context.session),
|
||||
last_used: started_at,
|
||||
};
|
||||
let (number_processes, pruned_entry) = {
|
||||
let pruned_entry = {
|
||||
let mut store = self.process_store.lock().await;
|
||||
let pruned_entry = Self::prune_processes_if_needed(&mut store);
|
||||
store.processes.insert(process_id, entry);
|
||||
(store.processes.len(), pruned_entry)
|
||||
pruned_entry
|
||||
};
|
||||
// prune_processes_if_needed runs while holding process_store; do async
|
||||
// network-approval cleanup only after dropping that lock.
|
||||
@@ -840,16 +839,6 @@ impl UnifiedExecProcessManager {
|
||||
pruned_entry.process.terminate();
|
||||
}
|
||||
|
||||
if number_processes >= WARNING_UNIFIED_EXEC_PROCESSES {
|
||||
context
|
||||
.session
|
||||
.record_model_warning(
|
||||
format!("The maximum number of unified exec processes you can keep open is {WARNING_UNIFIED_EXEC_PROCESSES} and you currently have {number_processes} processes open. Reuse older processes or close them to prevent automatic pruning of old processes"),
|
||||
&context.turn
|
||||
)
|
||||
.await;
|
||||
};
|
||||
|
||||
spawn_exit_watcher(
|
||||
Arc::clone(&process),
|
||||
Arc::clone(&context.session),
|
||||
|
||||
@@ -59,7 +59,7 @@ fn disabled_text_turn(test: &TestCodex, text: &str) -> Op {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn openai_model_header_mismatch_emits_warning_event_and_warning_item() -> Result<()> {
|
||||
async fn openai_model_header_mismatch_emits_warning_event() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
@@ -92,36 +92,6 @@ async fn openai_model_header_mismatch_emits_warning_event_and_warning_item() ->
|
||||
assert!(warning.message.contains(REQUESTED_MODEL));
|
||||
assert!(warning.message.contains(SERVER_MODEL));
|
||||
|
||||
let warning_item = wait_for_event(&test.codex, |event| {
|
||||
matches!(
|
||||
event,
|
||||
EventMsg::RawResponseItem(raw)
|
||||
if matches!(
|
||||
&raw.item,
|
||||
ResponseItem::Message { content, .. }
|
||||
if content.iter().any(|item| matches!(
|
||||
item,
|
||||
ContentItem::InputText { text } if text.starts_with("Warning: ")
|
||||
))
|
||||
)
|
||||
)
|
||||
})
|
||||
.await;
|
||||
let EventMsg::RawResponseItem(raw) = warning_item else {
|
||||
panic!("expected raw response item event");
|
||||
};
|
||||
let ResponseItem::Message { role, content, .. } = raw.item else {
|
||||
panic!("expected warning to be recorded as a message item");
|
||||
};
|
||||
assert_eq!(role, "user");
|
||||
let warning_text = content.iter().find_map(|item| match item {
|
||||
ContentItem::InputText { text } => Some(text.as_str()),
|
||||
_ => None,
|
||||
});
|
||||
let warning_text = warning_text.expect("warning message should include input_text content");
|
||||
assert!(warning_text.contains(REQUESTED_MODEL));
|
||||
assert!(warning_text.contains(SERVER_MODEL));
|
||||
|
||||
let _ = wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user