Compare commits

...

1 Commits

Author SHA1 Message Date
Eric Traut
22a4f3cad3 Preserve goal context during compaction 2026-05-11 09:55:04 -07:00
7 changed files with 69 additions and 6 deletions

View File

@@ -4,6 +4,7 @@ use std::time::Instant;
use crate::Prompt;
use crate::client::ModelClientSession;
use crate::client_common::ResponseEvent;
use crate::context::is_compaction_preserved_contextual_user_message_content;
use crate::hook_runtime::PostCompactHookOutcome;
use crate::hook_runtime::PreCompactHookOutcome;
use crate::hook_runtime::run_post_compact_hooks;
@@ -397,11 +398,22 @@ pub(crate) fn collect_user_messages(items: &[ResponseItem]) -> Vec<String> {
Some(user.message())
}
}
_ => None,
_ => compaction_preserved_contextual_user_message_text(item),
})
.collect()
}
fn compaction_preserved_contextual_user_message_text(item: &ResponseItem) -> Option<String> {
let ResponseItem::Message { role, content, .. } = item else {
return None;
};
if role == "user" && is_compaction_preserved_contextual_user_message_content(content) {
content_items_to_text(content)
} else {
None
}
}
pub(crate) fn is_summary_message(message: &str) -> bool {
message.starts_with(format!("{SUMMARY_PREFIX}\n").as_str())
}

View File

@@ -7,6 +7,7 @@ use crate::compact::CompactionAnalyticsAttempt;
use crate::compact::InitialContextInjection;
use crate::compact::compaction_status_from_result;
use crate::compact::insert_initial_context_before_last_real_user_or_summary;
use crate::context::is_compaction_preserved_contextual_user_message_content;
use crate::context_manager::ContextManager;
use crate::context_manager::TotalTokenUsageBreakdown;
use crate::context_manager::estimate_response_item_model_visible_bytes;
@@ -283,7 +284,8 @@ pub(crate) async fn process_compacted_history(
/// - `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.
/// while preserving real user messages, persisted hook prompts, and
/// compaction-preserved contextual user messages.
///
/// This intentionally keeps:
/// - `assistant` messages (future remote compaction models may emit them)
@@ -292,11 +294,11 @@ pub(crate) async fn process_compacted_history(
fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
match item {
ResponseItem::Message { role, .. } if role == "developer" => false,
ResponseItem::Message { role, .. } if role == "user" => {
ResponseItem::Message { role, content, .. } if role == "user" => {
matches!(
crate::event_mapping::parse_turn_item(item),
Some(TurnItem::UserMessage(_) | TurnItem::HookPrompt(_))
)
) || is_compaction_preserved_contextual_user_message_content(content)
}
ResponseItem::Message { role, .. } if role == "assistant" => true,
ResponseItem::Message { .. } => false,

View File

@@ -1,4 +1,6 @@
use super::*;
use crate::context::ContextualUserFragment;
use crate::context::GoalContext;
use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::WireApi;
use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
@@ -82,7 +84,11 @@ fn collect_user_messages_extracts_user_text_only() {
}
#[test]
fn collect_user_messages_filters_session_prefix_entries() {
fn collect_user_messages_filters_session_prefix_entries_and_preserves_goal_context() {
let goal_context = GoalContext {
prompt: "Continue working toward the active thread goal.".to_string(),
}
.render();
let items = vec![
ResponseItem::Message {
id: None,
@@ -113,11 +119,22 @@ do things
}],
phase: None,
},
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: goal_context.clone(),
}],
phase: None,
},
];
let collected = collect_user_messages(&items);
assert_eq!(vec!["real user message".to_string()], collected);
assert_eq!(
vec!["real user message".to_string(), goal_context],
collected
);
}
#[test]
@@ -353,6 +370,9 @@ keep me updated
#[tokio::test]
async fn process_compacted_history_inserts_context_before_last_real_user_message_only() {
let goal_context: ResponseItem = ContextualUserFragment::into(GoalContext {
prompt: "Continue working toward the active thread goal.".to_string(),
});
let compacted_history = vec![
ResponseItem::Message {
id: None,
@@ -378,6 +398,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
}],
phase: None,
},
goal_context.clone(),
];
let (refreshed, initial_context) = process_compacted_history_with_test_session(
@@ -412,6 +433,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
}],
phase: None,
});
expected.push(goal_context);
assert_eq!(refreshed, expected);
}

View File

@@ -43,6 +43,13 @@ fn is_standard_contextual_user_text(text: &str) -> bool {
.any(|fragment| fragment.matches_text(text))
}
/// Returns true for contextual user fragments that should survive compaction.
fn is_compaction_preserved_contextual_user_text(text: &str) -> bool {
CONTEXTUAL_USER_FRAGMENTS
.iter()
.any(|fragment| fragment.is_compaction_preserved_text(text))
}
pub(crate) fn is_contextual_user_fragment(content_item: &ContentItem) -> bool {
let ContentItem::InputText { text } = content_item else {
return false;
@@ -50,6 +57,18 @@ pub(crate) fn is_contextual_user_fragment(content_item: &ContentItem) -> bool {
parse_hook_prompt_fragment(text).is_some() || is_standard_contextual_user_text(text)
}
/// Returns true when message content includes any compaction-preserved contextual user fragment.
pub(crate) fn is_compaction_preserved_contextual_user_message_content(
content: &[ContentItem],
) -> bool {
content.iter().any(|content_item| {
let ContentItem::InputText { text } = content_item else {
return false;
};
is_compaction_preserved_contextual_user_text(text)
})
}
pub(crate) fn parse_visible_hook_prompt_message(
id: Option<&String>,
content: &[ContentItem],

View File

@@ -8,6 +8,7 @@ use std::marker::PhantomData;
/// fragments without constructing the concrete context payload.
pub(crate) trait FragmentRegistration: Sync {
fn matches_text(&self, text: &str) -> bool;
fn is_compaction_preserved_text(&self, text: &str) -> bool;
}
pub(crate) struct FragmentRegistrationProxy<T> {
@@ -26,6 +27,10 @@ impl<T: ContextualUserFragment> FragmentRegistration for FragmentRegistrationPro
fn matches_text(&self, text: &str) -> bool {
T::matches_text(text)
}
fn is_compaction_preserved_text(&self, text: &str) -> bool {
T::PRESERVE_DURING_COMPACTION && T::matches_text(text)
}
}
/// Context payload that is injected as a message fragment.
@@ -41,6 +46,7 @@ pub trait ContextualUserFragment {
const ROLE: &'static str;
const START_MARKER: &'static str;
const END_MARKER: &'static str;
const PRESERVE_DURING_COMPACTION: bool = false;
fn body(&self) -> String;

View File

@@ -11,6 +11,7 @@ impl ContextualUserFragment for GoalContext {
const ROLE: &'static str = "user";
const START_MARKER: &'static str = "<goal_context>";
const END_MARKER: &'static str = "</goal_context>";
const PRESERVE_DURING_COMPACTION: bool = true;
fn body(&self) -> String {
format!("\n{}\n", self.prompt)

View File

@@ -31,6 +31,7 @@ pub(crate) use apps_instructions::AppsInstructions;
pub(crate) use available_plugins_instructions::AvailablePluginsInstructions;
pub(crate) use available_skills_instructions::AvailableSkillsInstructions;
pub(crate) use collaboration_mode_instructions::CollaborationModeInstructions;
pub(crate) use contextual_user_message::is_compaction_preserved_contextual_user_message_content;
pub(crate) use contextual_user_message::is_contextual_user_fragment;
pub(crate) use contextual_user_message::parse_visible_hook_prompt_message;
pub(crate) use environment_context::EnvironmentContext;