Install full context in compaction replacement history (#51117)

## Why

Manual, pre-turn, and post-turn compaction previously deferred rebuilding initial context until the next turn. Their saved replacement history could omit refreshed instructions and skill catalogs.

## What changed

- Install initial context during local and remote compaction, before the last real user message or summary, keeping the compaction summary last.
- Persist matching full world-state and turn-context baselines with every replacement instead of retaining only partial extension metadata.
- Build replacement context and recompute token usage for the current model even when the previous model performs compaction. Reuse that captured context for eligible current-model retries.
- Capture manual compaction context centrally and propagate preparation errors to the task runner.

## Testing

Update assertions and snapshots for context ordering, refreshed instructions, resume and fork history, current-model checkpoint baselines, and cloud skill catalog retention across compaction and resume.

GitOrigin-RevId: 8572a76c31c6884a3c580b3bbb350d49af876bd1
This commit is contained in:
pakrym-oai
2026-10-05 16:10:27 +00:00
committed by copyberry
parent 823ea830c0
commit 8571b9eaa4
28 changed files with 513 additions and 584 deletions
+51 -68
View File
@@ -58,23 +58,6 @@ pub use codex_prompts::SUMMARIZATION_PROMPT;
pub use codex_prompts::SUMMARY_PREFIX;
const COMPACT_USER_MESSAGE_MAX_TOKENS: usize = 20_000;
/// Controls whether compaction replacement history must include initial context.
///
/// Pre-turn/manual compaction variants use `DoNotInject`: they replace history with a summary and
/// clear `reference_context_item`, so the next regular turn will fully reinject initial context
/// after compaction.
///
/// Mid-turn compaction must use `BeforeLastUserMessage` because the model is trained to see the
/// compaction summary as the last item in history after mid-turn compaction; we therefore inject
/// initial context into the replacement history just above the last real user message.
pub(crate) enum InitialContextInjection {
BeforeLastUserMessage {
world_state: Arc<WorldState>,
step_context: Arc<StepContext>,
},
DoNotInject,
}
/// Metadata for a new compaction checkpoint, kept separate from its replacement history.
///
/// `Session::replace_compacted_history` assigns missing item IDs before constructing the persisted
@@ -89,32 +72,28 @@ pub(crate) struct CompactedHistoryMetadata {
pub(crate) reviewer_compaction_hash: Option<String>,
}
/// Render replacement context and its comparison baseline from the same captured step.
/// Initial context is installed only after compaction returns, above the last real user message
/// (or the summary when no user message remains), keeping the compaction summary last.
pub(crate) async fn build_compaction_initial_context(
sess: &Session,
initial_context_injection: &InitialContextInjection,
) -> (Vec<ResponseItemEnvelope>, Option<WorldStateSnapshot>) {
// Return the rendered state with its items so history and its baseline stay identical.
match initial_context_injection {
InitialContextInjection::BeforeLastUserMessage {
world_state,
step_context,
} => {
let (items, snapshot) = sess
.build_initial_context_with_world_state(step_context, world_state.as_ref())
.await;
(
items.into_iter().map(ResponseItemEnvelope::new).collect(),
Some(snapshot),
)
}
InitialContextInjection::DoNotInject => (Vec::new(), None),
}
step_context: &StepContext,
world_state: &WorldState,
) -> (Vec<ResponseItemEnvelope>, WorldStateSnapshot) {
let (items, snapshot) = sess
.build_initial_context_with_world_state(step_context, world_state)
.await;
(
items.into_iter().map(ResponseItemEnvelope::new).collect(),
snapshot,
)
}
pub(crate) async fn run_inline_auto_compact_task(
sess: Arc<Session>,
turn_context: Arc<TurnContext>,
initial_context_injection: InitialContextInjection,
replacement_step_context: Arc<StepContext>,
world_state: Arc<WorldState>,
reason: CompactionReason,
phase: CompactionPhase,
) -> CodexResult<()> {
@@ -133,11 +112,15 @@ pub(crate) async fn run_inline_auto_compact_task(
run_compact_task_inner(
sess,
turn_context,
replacement_step_context,
input,
initial_context_injection,
CompactionTrigger::Auto,
reason,
phase,
world_state,
CompactionTurnMetadata::new(
CompactionTrigger::Auto,
reason,
CompactionImplementation::Responses,
phase,
),
)
.await?;
Ok(())
@@ -145,18 +128,22 @@ pub(crate) async fn run_inline_auto_compact_task(
pub(crate) async fn run_compact_task(
sess: Arc<Session>,
turn_context: Arc<TurnContext>,
step_context: Arc<StepContext>,
world_state: Arc<WorldState>,
input: Vec<UserInput>,
) -> CodexResult<()> {
sess.emit_turn_started(&turn_context).await;
run_compact_task_inner(
sess.clone(),
turn_context,
Arc::clone(&step_context.turn),
step_context,
input,
InitialContextInjection::DoNotInject,
CompactionTrigger::Manual,
CompactionReason::UserRequested,
CompactionPhase::StandaloneTurn,
world_state,
CompactionTurnMetadata::new(
CompactionTrigger::Manual,
CompactionReason::UserRequested,
CompactionImplementation::Responses,
CompactionPhase::StandaloneTurn,
),
)
.await?;
Ok(())
@@ -165,14 +152,14 @@ pub(crate) async fn run_compact_task(
async fn run_compact_task_inner(
sess: Arc<Session>,
turn_context: Arc<TurnContext>,
replacement_step_context: Arc<StepContext>,
input: Vec<UserInput>,
initial_context_injection: InitialContextInjection,
trigger: CompactionTrigger,
reason: CompactionReason,
phase: CompactionPhase,
world_state: Arc<WorldState>,
compaction_metadata: CompactionTurnMetadata,
) -> CodexResult<()> {
let compaction_metadata =
CompactionTurnMetadata::new(trigger, reason, CompactionImplementation::Responses, phase);
let trigger = compaction_metadata.trigger();
let reason = compaction_metadata.reason();
let phase = compaction_metadata.phase();
let attempt = CompactionAnalyticsAttempt::begin(
sess.as_ref(),
turn_context.as_ref(),
@@ -201,8 +188,9 @@ async fn run_compact_task_inner(
let result = run_compact_task_inner_impl(
Arc::clone(&sess),
Arc::clone(&turn_context),
replacement_step_context,
input,
initial_context_injection,
world_state,
compaction_metadata,
)
.await;
@@ -250,8 +238,9 @@ async fn run_compact_task_inner(
async fn run_compact_task_inner_impl(
sess: Arc<Session>,
turn_context: Arc<TurnContext>,
replacement_step_context: Arc<StepContext>,
input: Vec<UserInput>,
initial_context_injection: InitialContextInjection,
world_state: Arc<WorldState>,
compaction_metadata: CompactionTurnMetadata,
) -> CodexResult<String> {
let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new());
@@ -371,20 +360,13 @@ async fn run_compact_task_inner_impl(
let (window_number, window_ids) = sess.advance_auto_compact_window().await;
let (initial_context, world_state_baseline) =
build_compaction_initial_context(sess.as_ref(), &initial_context_injection).await;
if !initial_context.is_empty() {
new_history =
insert_initial_context_before_last_real_user_or_summary(new_history, initial_context);
}
let reference_context_item = match initial_context_injection {
InitialContextInjection::DoNotInject => None,
InitialContextInjection::BeforeLastUserMessage { step_context, .. } => {
Some(step_context.to_turn_context_item())
}
};
build_compaction_initial_context(sess.as_ref(), &replacement_step_context, &world_state)
.await;
new_history =
insert_initial_context_before_last_real_user_or_summary(new_history, initial_context);
sess.replace_compacted_history(
new_history,
reference_context_item,
replacement_step_context.to_turn_context_item(),
world_state_baseline,
CompactedHistoryMetadata {
input_goal_ids,
@@ -397,7 +379,8 @@ async fn run_compact_task_inner_impl(
},
)
.await;
sess.recompute_token_usage(&turn_context).await;
sess.recompute_token_usage(&replacement_step_context.turn)
.await;
sess.emit_turn_item_completed(&turn_context, compaction_item)
.await;
+19 -3
View File
@@ -1,3 +1,4 @@
use crate::session::turn_context::TurnContext;
use codex_analytics::CompactionImplementation;
use codex_analytics::CompactionReason;
use codex_otel::SessionTelemetry;
@@ -6,13 +7,28 @@ use codex_protocol::error::CodexErrorDetails;
use tracing::warn;
/// Returns whether a failed compaction attempt should use the current model.
pub(crate) fn should_retry_with_current_model(error: &CodexErr) -> bool {
!matches!(
pub(crate) fn should_retry_with_current_model(
error: &CodexErr,
previous: &TurnContext,
current: &TurnContext,
) -> bool {
if matches!(
error.details(),
CodexErrorDetails::TurnAborted
| CodexErrorDetails::Interrupted
| CodexErrorDetails::SessionBudgetExceeded
)
) || (previous.model_info().slug == current.model_info().slug
&& previous.cyber_access_program == current.cyber_access_program)
{
return false;
}
current.provider.info().is_openai()
&& current
.auth_manager
.as_deref()
.and_then(codex_login::AuthManager::auth_cached)
.is_some_and(|auth| auth.uses_codex_backend() || auth.is_api_key_auth())
}
pub(crate) fn record_model_fallback(
+24 -37
View File
@@ -7,7 +7,6 @@ use crate::client_common::ResponseEvent;
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;
@@ -15,6 +14,7 @@ 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::estimate_item_token_count;
use crate::hook_runtime::PostCompactHookOutcome;
use crate::hook_runtime::PreCompactHookOutcome;
@@ -56,7 +56,6 @@ use codex_rollout_trace::InferenceTraceContext;
use codex_utils_output_truncation::approx_token_count;
use codex_utils_output_truncation::truncate_text;
use futures::StreamExt;
use tokio_util::sync::CancellationToken;
#[path = "compact_remote_v2_attempt.rs"]
mod attempt;
@@ -81,9 +80,9 @@ const MAX_REMOTE_COMPACTION_V2_STREAM_RETRIES: u64 = 2;
pub(crate) async fn run_inline_remote_auto_compact_task(
sess: Arc<Session>,
step_context: Arc<StepContext>,
fallback_step_context: Option<Arc<StepContext>>,
replacement_step_context: Arc<StepContext>,
client_session: &mut ModelClientSession,
initial_context_injection: InitialContextInjection,
world_state: Arc<WorldState>,
reason: CompactionReason,
phase: CompactionPhase,
) -> CodexResult<()> {
@@ -96,9 +95,9 @@ pub(crate) async fn run_inline_remote_auto_compact_task(
run_remote_compact_task_inner(
&sess,
&step_context,
fallback_step_context.as_ref(),
&replacement_step_context,
Some(client_session),
initial_context_injection,
world_state,
compaction_metadata,
)
.await
@@ -106,14 +105,9 @@ pub(crate) async fn run_inline_remote_auto_compact_task(
pub(crate) async fn run_remote_compact_task(
sess: Arc<Session>,
turn_context: Arc<TurnContext>,
step_context: Arc<StepContext>,
world_state: Arc<WorldState>,
) -> 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?;
sess.emit_turn_started(&turn_context).await;
let compaction_metadata = CompactionTurnMetadata::new(
CompactionTrigger::Manual,
CompactionReason::UserRequested,
@@ -123,9 +117,9 @@ pub(crate) async fn run_remote_compact_task(
run_remote_compact_task_inner(
&sess,
&step_context,
/*fallback_step_context*/ None,
&step_context,
/*client_session*/ None,
InitialContextInjection::DoNotInject,
world_state,
compaction_metadata,
)
.await
@@ -134,9 +128,9 @@ pub(crate) async fn run_remote_compact_task(
async fn run_remote_compact_task_inner(
sess: &Arc<Session>,
step_context: &Arc<StepContext>,
fallback_step_context: Option<&Arc<StepContext>>,
replacement_step_context: &Arc<StepContext>,
client_session: Option<&mut ModelClientSession>,
initial_context_injection: InitialContextInjection,
world_state: Arc<WorldState>,
compaction_metadata: CompactionTurnMetadata,
) -> CodexResult<()> {
let turn_context = &step_context.turn;
@@ -176,9 +170,9 @@ async fn run_remote_compact_task_inner(
let result = run_remote_compact_task_inner_impl(
sess,
step_context,
fallback_step_context,
replacement_step_context,
client_session,
initial_context_injection,
world_state,
compaction_metadata,
&mut analytics_details,
)
@@ -222,9 +216,9 @@ async fn run_remote_compact_task_inner(
async fn run_remote_compact_task_inner_impl(
sess: &Arc<Session>,
step_context: &Arc<StepContext>,
fallback_step_context: Option<&Arc<StepContext>>,
replacement_step_context: &Arc<StepContext>,
mut client_session: Option<&mut ModelClientSession>,
initial_context_injection: InitialContextInjection,
world_state: Arc<WorldState>,
compaction_metadata: CompactionTurnMetadata,
analytics_details: &mut CompactionAnalyticsDetails,
) -> CodexResult<()> {
@@ -253,15 +247,12 @@ async fn run_remote_compact_task_inner_impl(
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) {
let fallback_turn_context = &replacement_step_context.turn;
if !should_retry_with_current_model(&error, turn_context, fallback_turn_context) {
return Err(error);
}
sess.set_last_known_step_context(fallback_step_context)
sess.set_last_known_step_context(replacement_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(),
@@ -271,7 +262,7 @@ async fn run_remote_compact_task_inner_impl(
);
let fallback_result = run_remote_compact_v2_attempt(
sess,
fallback_step_context,
replacement_step_context,
client_session,
&fallback_compaction_trace,
compaction_metadata,
@@ -323,16 +314,11 @@ async fn run_remote_compact_task_inner_impl(
analytics_details.retained_image_count = Some(retained_images);
let (new_window_number, new_window_ids) = sess.advance_auto_compact_window().await;
let (initial_context, world_state_baseline) =
build_compaction_initial_context(sess.as_ref(), &initial_context_injection).await;
build_compaction_initial_context(sess.as_ref(), replacement_step_context, &world_state)
.await;
let new_history =
insert_initial_context_before_last_real_user_or_summary(compacted_history, initial_context);
let reference_context_item = match initial_context_injection {
InitialContextInjection::DoNotInject => None,
InitialContextInjection::BeforeLastUserMessage { step_context, .. } => {
Some(step_context.to_turn_context_item())
}
};
if let Some(trace_input_history) = trace_input_history.as_deref() {
let replacement_history = new_history
.iter()
@@ -359,7 +345,7 @@ async fn run_remote_compact_task_inner_impl(
};
sess.replace_compacted_history(
new_history,
reference_context_item,
replacement_step_context.to_turn_context_item(),
world_state_baseline,
CompactedHistoryMetadata {
input_goal_ids,
@@ -372,7 +358,8 @@ async fn run_remote_compact_task_inner_impl(
},
)
.await;
sess.recompute_token_usage(compaction_turn_context).await;
sess.recompute_token_usage(&replacement_step_context.turn)
.await;
sess.emit_turn_item_completed(compaction_turn_context, compaction_item)
.await;
+6 -1
View File
@@ -157,10 +157,15 @@ async fn local_compaction_respects_tool_metadata_state(
]),
)
.await;
let step = session
.capture_step_context(turn, &tokio_util::sync::CancellationToken::new())
.await?;
let world_state = Arc::new(session.build_world_state_for_step(&step).await?);
// OpenAI identity keeps the client from removing passthrough for compatibility.
run_compact_task(
Arc::clone(&session),
turn,
step,
world_state,
vec![UserInput::Text {
text: "Summarize the conversation.".to_string(),
text_elements: Vec::new(),
+3 -18
View File
@@ -1,6 +1,5 @@
use std::sync::Arc;
use crate::compact::InitialContextInjection;
use crate::context::world_state::WorldState;
use crate::hook_runtime::PostCompactHookOutcome;
use crate::hook_runtime::PreCompactHookOutcome;
@@ -8,13 +7,11 @@ use crate::hook_runtime::run_post_compact_hooks;
use crate::hook_runtime::run_pre_compact_hooks;
use crate::session::session::Session;
use crate::session::step_context::StepContext;
use crate::session::turn_context::TurnContext;
use codex_analytics::CompactionTrigger;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::items::ContextCompactionItem;
use codex_protocol::items::TurnItem;
use tokio_util::sync::CancellationToken;
/// Runs token-budget manual compaction as a normal compaction lifecycle.
///
@@ -23,15 +20,9 @@ use tokio_util::sync::CancellationToken;
/// observe the same lifecycle as local or remote compaction.
pub(crate) async fn run_manual_compact_task(
sess: Arc<Session>,
turn_context: Arc<TurnContext>,
step_context: Arc<StepContext>,
world_state: Arc<WorldState>,
) -> CodexResult<()> {
sess.emit_turn_started(&turn_context).await;
// Manual compaction runs outside run_turn, so it captures its own current step.
let step_context = sess
.capture_step_context(Arc::clone(&turn_context), &CancellationToken::new())
.await?;
let world_state = Arc::new(sess.build_world_state_for_step(&step_context).await?);
run_compact_task_inner(&sess, &step_context, world_state, CompactionTrigger::Manual).await
}
@@ -43,14 +34,8 @@ pub(crate) async fn run_manual_compact_task(
pub(crate) async fn run_inline_auto_compact_task(
sess: Arc<Session>,
step_context: Arc<StepContext>,
initial_context_injection: InitialContextInjection,
world_state: Arc<WorldState>,
) -> CodexResult<()> {
let world_state = match initial_context_injection {
InitialContextInjection::BeforeLastUserMessage { world_state, .. } => world_state,
InitialContextInjection::DoNotInject => {
Arc::new(sess.build_world_state_for_step(&step_context).await?)
}
};
run_compact_task_inner(&sess, &step_context, world_state, CompactionTrigger::Auto).await
}
@@ -82,8 +82,8 @@ async fn run_review_preserves_evidence_during_parent_compaction() {
parent
.replace_compacted_history(
vec![checkpoint.into()],
/*reference_context_item*/ None,
/*world_state_baseline*/ None,
turn.to_turn_context_item(),
crate::context::world_state::WorldStateSnapshot::default(),
crate::compact::CompactedHistoryMetadata {
input_goal_ids: Default::default(),
message: String::new(),
+2 -2
View File
@@ -2654,8 +2654,8 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow:
.into_iter()
.map(codex_history::ResponseItemEnvelope::new)
.collect(),
/*reference_context_item*/ None,
/*world_state_baseline*/ None,
turn.to_turn_context_item(),
crate::context::world_state::WorldStateSnapshot::default(),
crate::compact::CompactedHistoryMetadata {
input_goal_ids: Default::default(),
message: String::new(),
+16 -35
View File
@@ -3993,8 +3993,8 @@ impl Session {
pub(crate) async fn replace_compacted_history(
&self,
mut items: Vec<ResponseItemEnvelope>,
reference_context_item: Option<TurnContextItem>,
world_state_baseline: Option<WorldStateSnapshot>,
reference_context_item: TurnContextItem,
world_state_baseline: WorldStateSnapshot,
metadata: CompactedHistoryMetadata,
) {
for envelope in &mut items {
@@ -4024,20 +4024,9 @@ impl Session {
// Wait for accepted updates to finish persisting, then keep later updates from
// overtaking the current settings snapshot while its checkpoint is written.
let _settings_guard = thread_settings::acquire_persistence_lock(self).await;
// A new history window needs a full checkpoint, even when it contains only
// extension metadata and model-visible context will be rebuilt on the next turn.
let mut world_state_item = None;
let world_state_item = WorldStateItem::full(world_state_baseline.clone().into_object());
let compacted_item = {
let mut state = self.state.lock().await;
let snapshot = world_state_baseline.or_else(|| {
let previous = state.history.world_state_checkpoint()?;
let mut retained = serde_json::Map::new();
for contributor in self.services.extensions.context_contributors() {
retained
.extend(contributor.retain_world_state_after_compaction(&previous.state));
}
(!retained.is_empty()).then(|| WorldStateSnapshot::from(&retained))
});
// Goal edits are published outside the running task. Keep edits accepted after
// the compaction input snapshot, in their original order, after its replacement.
let replacement_goal_ids = crate::context::UserGoalUpdate::message_ids(
@@ -4060,16 +4049,13 @@ impl Session {
let replacement_history = items.clone();
state.replace_annotated_history(
items,
reference_context_item.clone(),
Some(reference_context_item.clone()),
HistoryReplacement::Compaction {
reviewer_compaction_hash: metadata.reviewer_compaction_hash,
},
);
state.reasoning_effort_pin = ReasoningEffortPin::Compacted;
if let Some(snapshot) = snapshot {
world_state_item = Some(WorldStateItem::full(snapshot.clone().into_object()));
state.history.set_world_state_baseline(snapshot);
}
state.history.set_world_state_baseline(world_state_baseline);
CompactedItem {
message: metadata.message,
replacement_history: Some(replacement_history),
@@ -4093,18 +4079,13 @@ impl Session {
}
};
let mut rollout_items = vec![RolloutItem::Compacted(compacted_item)];
// Persist the baseline after the replacement history that established it.
if let Some(world_state_item) = world_state_item {
rollout_items.push(RolloutItem::WorldState(world_state_item));
}
if let Some(turn_context_item) = reference_context_item {
rollout_items.push(RolloutItem::TurnContext(turn_context_item));
}
// The frozen turn context must not override current settings in persisted metadata.
rollout_items.push(RolloutItem::EventMsg(
thread_settings::applied_event(self).await,
));
let rollout_items = [
RolloutItem::Compacted(compacted_item),
RolloutItem::WorldState(world_state_item),
RolloutItem::TurnContext(reference_context_item),
// The frozen turn context must not override current settings in persisted metadata.
RolloutItem::EventMsg(thread_settings::applied_event(self).await),
];
if self.persist_rollout_items(&rollout_items).await
&& let Some(revision) = mcp_revision
{
@@ -4573,8 +4554,8 @@ impl Session {
let turn_context_item = step_context.to_turn_context_item();
self.replace_compacted_history(
context_items,
Some(turn_context_item),
Some(world_state_snapshot),
turn_context_item,
world_state_snapshot,
CompactedHistoryMetadata {
input_goal_ids,
message: String::new(),
@@ -4605,8 +4586,8 @@ impl Session {
/// `<model_switch>` developer message so model-specific instructions are not lost.
///
/// This is the normal runtime path that establishes a new `reference_context_item`.
/// Mid-turn compaction is the other path that can re-establish that reference when it
/// reinjects full initial context into replacement history. Live world-state changes may
/// Compaction also establishes that reference when it installs full initial context
/// into replacement history. Live world-state changes may
/// independently advance their in-memory baseline within a turn.
#[instrument(level = "trace", skip_all)]
pub(crate) async fn record_context_updates_and_set_reference_context_item(
+52 -70
View File
@@ -15,7 +15,6 @@ use super::*;
mod config_refresh_tests;
use crate::agents_md_manager::AgentsMdManager;
use crate::agents_md_manager::SessionInstructions;
use crate::compact::InitialContextInjection;
use crate::config::ConfigBuilder;
use crate::config::ConfigOverrides;
use crate::config::RuntimeConfigRefresh;
@@ -5854,7 +5853,7 @@ async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_up
#[tokio::test]
async fn settings_checkpoint_waits_for_accepted_settings_persistence() {
let (mut session, _turn_context, rx) = make_session_and_context_with_auth_and_config_and_rx(
let (mut session, turn_context, rx) = make_session_and_context_with_auth_and_config_and_rx(
CodexAuth::from_api_key("Test API Key"),
Vec::new(),
|config| {
@@ -5892,8 +5891,8 @@ async fn settings_checkpoint_waits_for_accepted_settings_persistence() {
let mut checkpoint = Box::pin(tokio::task::unconstrained(
session.replace_compacted_history(
vec![ResponseItemEnvelope::new(user_message("compacted history"))],
/*reference_context_item*/ None,
/*world_state_baseline*/ None,
turn_context.to_turn_context_item(),
WorldStateSnapshot::default(),
CompactedHistoryMetadata {
input_goal_ids: Default::default(),
message: "summary".to_string(),
@@ -6026,8 +6025,8 @@ async fn mcp_attribution_checkpoints_cover_batch_prefixes_compaction_and_restore
session
.replace_compacted_history(
vec![ResponseItemEnvelope::new(user_message("compacted history"))],
/*reference_context_item*/ None,
/*world_state_baseline*/ None,
turn_context.to_turn_context_item(),
WorldStateSnapshot::default(),
CompactedHistoryMetadata {
input_goal_ids: Default::default(),
message: "summary".to_string(),
@@ -6154,48 +6153,39 @@ async fn compaction_persists_resume_metadata_and_companion_records() {
thread_settings: session.thread_settings_snapshot().await,
};
let mut first_live_history = None;
for with_baselines in [true, false] {
let input_goal_ids =
crate::context::UserGoalUpdate::message_ids(session.clone_history().await.raw_items());
// The goal edit is accepted after the compaction input was captured.
let accepted_goal = if with_baselines {
session
.record_user_goal_update(crate::context::UserGoalUpdate::Clear)
.await
.expect("record concurrent clear");
session
.clone_history()
.await
.annotated_items()
.last()
.cloned()
} else {
None
};
let (window_number, window_ids) = session.advance_auto_compact_window().await;
session
.replace_compacted_history(
vec![ResponseItemEnvelope::new(user_message("compacted context"))],
with_baselines.then_some(turn_context_baseline.clone()),
with_baselines.then(|| world_state.render_full().0),
CompactedHistoryMetadata {
input_goal_ids,
message: String::new(),
window_number,
window_ids,
compaction_response_id: None,
compaction_model_hash: None,
reviewer_compaction_hash: None,
},
)
.await;
if let Some(accepted_goal) = accepted_goal {
let live_history = session.clone_history().await.annotated_items().to_vec();
assert_eq!(&live_history[1..], &[accepted_goal]);
first_live_history = Some(live_history);
}
}
let input_goal_ids =
crate::context::UserGoalUpdate::message_ids(session.clone_history().await.raw_items());
// The goal edit is accepted after the compaction input was captured.
session
.record_user_goal_update(crate::context::UserGoalUpdate::Clear)
.await
.expect("record concurrent clear");
let accepted_goal = session
.clone_history()
.await
.annotated_items()
.last()
.cloned()
.expect("accepted goal");
let (window_number, window_ids) = session.advance_auto_compact_window().await;
session
.replace_compacted_history(
vec![ResponseItemEnvelope::new(user_message("compacted context"))],
turn_context_baseline.clone(),
world_state.render_full().0,
CompactedHistoryMetadata {
input_goal_ids,
message: String::new(),
window_number,
window_ids,
compaction_response_id: None,
compaction_model_hash: None,
reviewer_compaction_hash: None,
},
)
.await;
let live_history = session.clone_history().await.annotated_items().to_vec();
assert_eq!(&live_history[1..], &[accepted_goal]);
session.flush_rollout().await.expect("flush checkpoints");
let (items, _, _) = RolloutRecorder::load_rollout_items(&rollout_path)
@@ -6206,26 +6196,22 @@ async fn compaction_persists_resume_metadata_and_companion_records() {
.skip_while(|item| !matches!(item, RolloutItem::Compacted(_)))
.collect::<Vec<_>>();
let [
RolloutItem::Compacted(first),
RolloutItem::WorldState(first_world_state),
RolloutItem::TurnContext(first_turn_context),
RolloutItem::EventMsg(EventMsg::ThreadSettingsApplied(first_settings)),
RolloutItem::Compacted(second),
RolloutItem::EventMsg(EventMsg::ThreadSettingsApplied(second_settings)),
RolloutItem::Compacted(compacted),
RolloutItem::WorldState(saved_world_state),
RolloutItem::TurnContext(saved_turn_context),
RolloutItem::EventMsg(EventMsg::ThreadSettingsApplied(saved_settings)),
] = compaction_items.as_slice()
else {
panic!("unexpected compaction records: {compaction_items:#?}");
};
assert_eq!(first.replacement_history, first_live_history);
assert_eq!(first.resume_metadata.as_ref(), Some(&expected));
assert_eq!(second.resume_metadata.as_ref(), Some(&expected));
assert_eq!(compacted.replacement_history, Some(live_history));
assert_eq!(compacted.resume_metadata.as_ref(), Some(&expected));
assert_eq!(
first_world_state,
saved_world_state,
&WorldStateItem::full(world_state.render_full().0.into_object())
);
assert_eq!(first_turn_context, &turn_context_baseline);
assert_eq!(first_settings, &expected_settings);
assert_eq!(second_settings, &expected_settings);
assert_eq!(saved_turn_context, &turn_context_baseline);
assert_eq!(saved_settings, &expected_settings);
}
#[tokio::test]
@@ -11312,12 +11298,8 @@ async fn build_initial_context_uses_retained_step_after_model_change() {
.await
.unwrap();
let world_a = Arc::new(session.build_world_state_for_step(&step_a).await.unwrap());
let retained = crate::compact::InitialContextInjection::BeforeLastUserMessage {
world_state: Arc::clone(&world_a),
step_context: Arc::clone(&step_a),
};
let (initial_a, _) =
crate::compact::build_compaction_initial_context(&session, &retained).await;
crate::compact::build_compaction_initial_context(&session, &step_a, &world_a).await;
let mut selected_b = step_a.settings.selected().clone();
selected_b.collaboration_mode.settings.model = "model-b".to_string();
@@ -11348,10 +11330,10 @@ async fn build_initial_context_uses_retained_step_after_model_change() {
.0;
let turn_contributions_b = session.build_turn_context_contribution_items(&step_b).await;
let (restored_a, restored_snapshot) =
crate::compact::build_compaction_initial_context(&session, &retained).await;
crate::compact::build_compaction_initial_context(&session, &step_a, &world_a).await;
assert_eq!(restored_a, initial_a);
assert_eq!(restored_snapshot, Some(world_a.render_full().0));
assert_eq!(restored_snapshot, world_a.render_full().0);
let initial_a = initial_a
.into_iter()
.map(ResponseItemEnvelope::into_item)
@@ -11943,9 +11925,9 @@ async fn remote_compaction_v2_retains_only_the_selected_step(first_attempt: Firs
crate::compact_remote_v2::run_inline_remote_auto_compact_task(
Arc::clone(&session),
Arc::clone(&primary),
Some(Arc::clone(&fallback)),
Arc::clone(&fallback),
&mut client_session,
InitialContextInjection::DoNotInject,
Arc::new(session.build_world_state_for_step(&fallback).await.unwrap()),
CompactionReason::ModelDownshift,
CompactionPhase::PreTurn,
)
@@ -1,5 +1,4 @@
use super::*;
use crate::compact::InitialContextInjection;
use crate::config::Constrained;
use crate::exec_policy::ExecPolicyManager;
use crate::guardian::GUARDIAN_REVIEWER_NAME;
@@ -1118,13 +1117,8 @@ async fn compaction_initial_context_preserves_separate_guardian_developer_messag
.await
.expect("world state should build"),
);
let initial_context_injection = InitialContextInjection::BeforeLastUserMessage {
world_state,
step_context,
};
let (refreshed, _) =
crate::compact::build_compaction_initial_context(&session, &initial_context_injection)
crate::compact::build_compaction_initial_context(&session, &step_context, &world_state)
.await;
let developer_messages = refreshed
+72 -117
View File
@@ -6,12 +6,12 @@ use std::sync::atomic::Ordering;
use crate::client::ModelClientSession;
use crate::client_common::Prompt;
use crate::client_common::ResponseEvent;
use crate::compact::InitialContextInjection;
use crate::compact::run_inline_auto_compact_task;
use crate::compact_remote_v2::run_inline_remote_auto_compact_task as run_inline_remote_auto_compact_task_v2;
use crate::connectors;
use crate::context::ContextualUserFragment;
use crate::context::UserVerificationNotice;
use crate::context::world_state::WorldState;
use crate::cyber_access_program;
use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::feedback_tags;
@@ -346,9 +346,9 @@ pub(crate) async fn run_turn(
run_auto_compact(
&sess,
Arc::clone(&first_step_context),
/*fallback_step_context*/ None,
Arc::clone(&first_step_context),
&mut client_session,
InitialContextInjection::DoNotInject,
Arc::clone(&world_state),
CompactionReason::ContextLimit,
CompactionPhase::PreTurn,
)
@@ -614,12 +614,9 @@ pub(crate) async fn run_turn(
if let Err(err) = run_auto_compact(
&sess,
Arc::clone(&step_context),
/*fallback_step_context*/ None,
Arc::clone(&step_context),
&mut client_session,
InitialContextInjection::BeforeLastUserMessage {
world_state: Arc::clone(&world_state),
step_context: Arc::clone(&step_context),
},
Arc::clone(&world_state),
CompactionReason::ContextLimit,
CompactionPhase::MidTurn,
)
@@ -726,9 +723,9 @@ pub(crate) async fn run_turn(
&& let Err(err) = run_auto_compact(
&sess,
Arc::clone(&step_context),
/*fallback_step_context*/ None,
Arc::clone(&step_context),
&mut client_session,
InitialContextInjection::DoNotInject,
Arc::clone(&world_state),
CompactionReason::ContextLimit,
CompactionPhase::PostTurn,
)
@@ -777,12 +774,9 @@ pub(crate) async fn run_turn(
run_auto_compact(
&sess,
Arc::clone(&step_context),
/*fallback_step_context*/ None,
Arc::clone(&step_context),
&mut client_session,
InitialContextInjection::BeforeLastUserMessage {
world_state: Arc::clone(&world_state),
step_context: Arc::clone(&step_context),
},
Arc::clone(&world_state),
CompactionReason::ContextLimit,
CompactionPhase::MidTurn,
)
@@ -1318,12 +1312,13 @@ async fn run_pre_sampling_compact(
let step_context = sess
.capture_step_context(Arc::clone(turn_context), cancellation_token)
.await?;
let world_state = Arc::new(sess.build_world_state_for_step(&step_context).await?);
run_auto_compact(
sess,
Arc::clone(&step_context),
step_context,
/*fallback_step_context*/ None,
client_session,
InitialContextInjection::DoNotInject,
world_state,
CompactionReason::ContextLimit,
CompactionPhase::PreTurn,
)
@@ -1340,33 +1335,6 @@ fn comp_hash_changed(previous: Option<&str>, current: Option<&str>) -> bool {
.is_some_and(|(previous, current)| previous != current)
}
/// Captures the current model's request-scoped state for retrying previous-model compaction.
///
/// Returns `None` when auth uses neither the Codex backend nor an API key, the provider is
/// not OpenAI, or the previous and current model/program pairs are the same.
async fn capture_current_model_fallback_step_context(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
previous_turn_context: &TurnContext,
cancellation_token: &CancellationToken,
) -> CodexResult<Option<Arc<StepContext>>> {
let supports_fallback = turn_context
.auth_manager
.as_deref()
.and_then(codex_login::AuthManager::auth_cached)
.is_some_and(|auth| auth.uses_codex_backend() || auth.is_api_key_auth());
if !supports_fallback
|| !turn_context.provider.info().is_openai()
|| (previous_turn_context.model_info().slug == turn_context.model_info().slug
&& previous_turn_context.cyber_access_program == turn_context.cyber_access_program)
{
return Ok(None);
}
sess.capture_speculative_step_context(Arc::clone(turn_context), cancellation_token)
.await
.map(Some)
}
/// Runs pre-sampling compaction against the previous model when its compaction compatibility
/// hash changed or when switching to a smaller context-window model.
///
@@ -1401,79 +1369,65 @@ async fn maybe_run_previous_model_inline_compact(
);
let previous_model_turn_context = Arc::new(previous_model_turn_context);
if should_compact_for_comp_hash_change {
let step_context = sess
.capture_step_context(Arc::clone(&previous_model_turn_context), cancellation_token)
.await?;
let fallback_step_context = capture_current_model_fallback_step_context(
sess,
turn_context,
&previous_model_turn_context,
cancellation_token,
)
.await?;
run_auto_compact(
sess,
step_context,
fallback_step_context,
client_session,
InitialContextInjection::DoNotInject,
CompactionReason::CompHashChanged,
CompactionPhase::PreTurn,
)
.await?;
return Ok(());
}
let Some(old_context_window) = previous_model_turn_context.model_context_window() else {
return Ok(());
};
let Some(new_context_window) = turn_context.model_context_window() else {
return Ok(());
};
let active_context_tokens = sess.get_total_token_usage().await;
let previous_model_limit_reached = match turn_context
.config
.model_auto_compact_token_limit_scope
{
AutoCompactTokenLimitScope::Total => {
let new_auto_compact_limit = turn_context
.model_info()
.auto_compact_token_limit()
.unwrap_or(i64::MAX);
active_context_tokens > new_auto_compact_limit
|| active_context_tokens >= new_context_window
let reason = if should_compact_for_comp_hash_change {
CompactionReason::CompHashChanged
} else {
let Some(old_context_window) = previous_model_turn_context.model_context_window() else {
return Ok(());
};
let Some(new_context_window) = turn_context.model_context_window() else {
return Ok(());
};
let active_context_tokens = sess.get_total_token_usage().await;
let previous_model_limit_reached =
match turn_context.config.model_auto_compact_token_limit_scope {
AutoCompactTokenLimitScope::Total => {
let new_auto_compact_limit = turn_context
.model_info()
.auto_compact_token_limit()
.unwrap_or(i64::MAX);
active_context_tokens > new_auto_compact_limit
|| active_context_tokens >= new_context_window
}
AutoCompactTokenLimitScope::BodyAfterPrefix => {
active_context_tokens >= new_context_window
}
};
if !previous_model_limit_reached
|| previous_model_turn_context.model_info().slug == turn_context.model_info().slug
|| old_context_window <= new_context_window
{
return Ok(());
}
AutoCompactTokenLimitScope::BodyAfterPrefix => active_context_tokens >= new_context_window,
CompactionReason::ModelDownshift
};
let should_run = previous_model_limit_reached
&& previous_model_turn_context.model_info().slug != turn_context.model_info().slug
&& old_context_window > new_context_window;
if should_run {
let step_context = sess
.capture_step_context(Arc::clone(&previous_model_turn_context), cancellation_token)
.await?;
let fallback_step_context = capture_current_model_fallback_step_context(
sess,
turn_context,
&previous_model_turn_context,
cancellation_token,
)
let step_context = sess
.capture_step_context(previous_model_turn_context, cancellation_token)
.await?;
run_auto_compact(
sess,
step_context,
fallback_step_context,
client_session,
InitialContextInjection::DoNotInject,
CompactionReason::ModelDownshift,
CompactionPhase::PreTurn,
)
// The previous model summarizes the old window; the current model owns its replacement.
// Reuse the replacement step if the compaction request needs to fall back to that model.
let replacement_step_context = sess
.capture_speculative_step_context(Arc::clone(turn_context), cancellation_token)
.await?;
}
let world_state = Arc::new(
sess.build_world_state_for_step(&replacement_step_context)
.await?,
);
run_auto_compact(
sess,
step_context,
replacement_step_context,
client_session,
world_state,
reason,
CompactionPhase::PreTurn,
)
.await?;
Ok(())
}
/// `world_state` belongs to `replacement_step_context`, which may differ from the compactor.
#[instrument(
level = "trace",
skip_all,
@@ -1482,9 +1436,9 @@ async fn maybe_run_previous_model_inline_compact(
async fn run_auto_compact(
sess: &Arc<Session>,
step_context: Arc<StepContext>,
fallback_step_context: Option<Arc<StepContext>>,
replacement_step_context: Arc<StepContext>,
client_session: &mut ModelClientSession,
initial_context_injection: InitialContextInjection,
world_state: Arc<WorldState>,
reason: CompactionReason,
phase: CompactionPhase,
) -> CodexResult<()> {
@@ -1501,8 +1455,8 @@ async fn run_auto_compact(
// instead of consuming a pending `new_context` tool request.
crate::compact_token_budget::run_inline_auto_compact_task(
Arc::clone(sess),
step_context,
initial_context_injection,
replacement_step_context,
world_state,
)
.await?;
return Ok(());
@@ -1518,9 +1472,9 @@ async fn run_auto_compact(
run_inline_remote_auto_compact_task_v2(
Arc::clone(sess),
step_context,
fallback_step_context,
replacement_step_context,
client_session,
initial_context_injection,
world_state,
reason,
phase,
)
@@ -1535,7 +1489,8 @@ async fn run_auto_compact(
run_inline_auto_compact_task(
Arc::clone(sess),
Arc::clone(turn_context),
initial_context_injection,
replacement_step_context,
world_state,
reason,
phase,
)
+21 -5
View File
@@ -31,7 +31,7 @@ impl SessionTask for CompactTask {
session: Arc<Session>,
ctx: Arc<TurnContext>,
_input: Vec<TurnInput>,
_cancellation_token: CancellationToken,
cancellation_token: CancellationToken,
) -> SessionTaskResult {
let _profile_guard = ctx.turn_timing_state.begin_compaction();
let _compaction_span = tracing::trace_span!(
@@ -40,8 +40,19 @@ impl SessionTask for CompactTask {
conversation.id = %session.thread_id,
turn.id = %ctx.sub_id,
);
// Preparation errors must reach the task runner, which reports them to the client.
session.emit_turn_started(&ctx).await;
let step_context = session
.capture_step_context(Arc::clone(&ctx), &cancellation_token)
.await?;
let world_state = Arc::new(session.build_world_state_for_step(&step_context).await?);
if ctx.config.features.enabled(Feature::TokenBudget) {
crate::compact_token_budget::run_manual_compact_task(session, ctx).await?;
crate::compact_token_budget::run_manual_compact_task(
session,
step_context,
world_state,
)
.await?;
return Ok(None);
}
@@ -52,8 +63,12 @@ impl SessionTask for CompactTask {
"remote_v2",
/*manual*/ true,
);
crate::compact_remote_v2::run_remote_compact_task(session.clone(), Arc::clone(&ctx))
.await
crate::compact_remote_v2::run_remote_compact_task(
session.clone(),
step_context,
world_state,
)
.await
}
RemoteCompactionSupport::Unsupported => {
emit_compact_metric(
@@ -71,7 +86,8 @@ impl SessionTask for CompactTask {
// Compaction prompt is synthesized; no UI element ranges to preserve.
text_elements: Vec::new(),
}];
crate::compact::run_compact_task(session.clone(), Arc::clone(&ctx), input).await
crate::compact::run_compact_task(session.clone(), step_context, world_state, input)
.await
}
};
if let Err(err) = result {
+10 -15
View File
@@ -735,7 +735,7 @@ async fn summarize_context_three_requests_and_instructions(
// Verify rollout contains user-turn TurnContext entries and a Compacted entry.
println!("rollout path: {}", rollout_path.display());
let text = std::fs::read_to_string(&rollout_path).expect("failed to read rollout file");
let mut regular_turn_context_count = 0usize;
let mut turn_context_count = 0usize;
let mut saw_compacted_summary = false;
for line in text.lines() {
let trimmed = line.trim();
@@ -747,7 +747,7 @@ async fn summarize_context_three_requests_and_instructions(
};
match entry.item {
RolloutItem::TurnContext(_) => {
regular_turn_context_count += 1;
turn_context_count += 1;
}
RolloutItem::Compacted(ci) if ci.message == expected_summary_message => {
let summary_item = ci
@@ -780,8 +780,8 @@ async fn summarize_context_three_requests_and_instructions(
}
assert_eq!(
regular_turn_context_count, 2,
"rollout should contain one TurnContext entry per real user turn"
turn_context_count, 3,
"rollout should contain two user-turn contexts and the compaction baseline"
);
assert!(
saw_compacted_summary,
@@ -4463,15 +4463,10 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
final_request_last_user_text, final_user_message,
"final turn request should end with the submitted user message"
);
let history_before_seeded_prefix = final_request_before_last_user
.strip_suffix(initial_seeded_user_prefix)
.expect("final request should end with the seeded user prefix from the first request");
let expected_history = vec![
first_user_message.to_string(),
second_user_message.to_string(),
expected_second_summary,
];
assert_eq!(history_before_seeded_prefix, expected_history.as_slice());
let mut expected_history = vec![first_user_message.to_string()];
expected_history.extend_from_slice(initial_seeded_user_prefix);
expected_history.extend([second_user_message.to_string(), expected_second_summary]);
assert_eq!(final_request_before_last_user, expected_history.as_slice());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -5825,8 +5820,8 @@ async fn remote_v2_compaction_refreshes_instructions_and_preserves_them_on_cold_
let replacement_history = replacement_history_from_rollout(&rollout_path)?;
assert_eq!(
instruction_fragments_in_items(&replacement_history),
Vec::<String>::new(),
"remote-v2 replacement history currently omits the global-instruction fragment"
vec![new_fragment.clone()],
"remote-v2 replacement history includes refreshed global instructions"
);
assert_eq!(
test.codex.instruction_sources().await,
@@ -17,7 +17,6 @@ enum History {
Fork,
Rollback,
IncompleteRollback,
Checkpoint,
ApiKeyResume,
ApiKeyEnabledResume,
ApiKeyCustomProviderResume,
@@ -71,7 +70,6 @@ async fn submit_pair(
#[test_case(Compaction::RemoteHash, History::Fork, Some(CyberAccessProgram::DaybreakBlue); "fork")]
#[test_case(Compaction::RemoteHash, History::Rollback, Some(CyberAccessProgram::DaybreakBlue); "rollback")]
#[test_case(Compaction::RemoteHash, History::IncompleteRollback, Some(CyberAccessProgram::DaybreakBlue); "incomplete rollback")]
#[test_case(Compaction::RemoteHash, History::Checkpoint, Some(CyberAccessProgram::DaybreakBlue); "checkpoint")]
#[test_case(Compaction::RemoteHash, History::Resume, None; "missing program")]
#[test_case(Compaction::RemoteHash, History::Resume, Some(CyberAccessProgram::Standard); "standard")]
#[test_case(Compaction::LocalHash, History::Live, None; "local missing program")]
@@ -165,30 +163,6 @@ async fn model_switch_program_pair(
discarded_model
);
}
if matches!(history, History::Checkpoint) {
let compact = mount_sse_once(
&server,
sse(vec![
json!({"type": "response.output_item.done", "item": {
"type": "compaction", "encrypted_content": "CHECKPOINT"
}}),
ev_completed("checkpoint"),
]),
)
.await;
initial.codex.submit(Op::Compact).await?;
wait_for_event(&initial.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
.await;
assert_eq!(
compact
.single_request()
.inputs_of_type("compaction_trigger")
.len(),
1
);
}
let resumed;
let thread = match history {
@@ -210,7 +184,6 @@ async fn model_switch_program_pair(
History::Resume
| History::Rollback
| History::IncompleteRollback
| History::Checkpoint
| History::ApiKeyResume
| History::ApiKeyEnabledResume
| History::ApiKeyCustomProviderResume => {
@@ -368,6 +341,39 @@ async fn model_switch_program_pair(
if !local {
assert_eq!(requests[0].inputs_of_type("compaction_trigger").len(), 1);
}
// Even when the previous model summarizes, the replacement belongs to the current model.
let rollout = fs::read_to_string(thread.rollout_path().expect("rollout"))?
.lines()
.map(codex_rollout::parse_rollout_line)
.collect::<Result<Vec<_>, _>>()?;
let checkpoint = rollout
.iter()
.rposition(|line| matches!(&line.item, RolloutItem::Compacted(_)))
.expect("model-switch compaction");
let replacement = &rollout[checkpoint + 1..];
let world_state = replacement
.iter()
.find_map(|line| match &line.item {
RolloutItem::WorldState(state) => Some(state),
_ => None,
})
.expect("replacement world state");
let turn_context = replacement
.iter()
.find_map(|line| match &line.item {
RolloutItem::TurnContext(context) => Some(context),
_ => None,
})
.expect("replacement turn context");
assert_eq!(
(
world_state.full,
world_state.state.get("model"),
turn_context.model.as_str(),
),
(true, Some(&json!(next_model)), next_model),
);
Ok(())
}
@@ -277,18 +277,16 @@ async fn compact_resume_and_fork_preserve_model_history_view() {
let summary_after_compact = extract_summary_user_text(&requests[2], SUMMARY_TEXT);
let summary_after_resume = extract_summary_user_text(&requests[3], SUMMARY_TEXT);
let summary_after_fork = extract_summary_user_text(&requests[4], SUMMARY_TEXT);
let mut expected_after_compact_user_texts =
vec!["hello world".to_string(), summary_after_compact];
expected_after_compact_user_texts.extend_from_slice(seeded_user_prefix);
let mut expected_after_compact_user_texts = seeded_user_prefix.to_vec();
expected_after_compact_user_texts.extend(["hello world".to_string(), summary_after_compact]);
expected_after_compact_user_texts.push("AFTER_COMPACT".to_string());
assert_eq!(
json_message_input_texts(&requests[2], "user"),
expected_after_compact_user_texts
);
let mut expected_after_resume_user_texts =
vec!["hello world".to_string(), summary_after_resume];
expected_after_resume_user_texts.extend_from_slice(seeded_user_prefix);
let mut expected_after_resume_user_texts = seeded_user_prefix.to_vec();
expected_after_resume_user_texts.extend(["hello world".to_string(), summary_after_resume]);
expected_after_resume_user_texts.push("AFTER_COMPACT".to_string());
let after_resume_user_texts = json_message_input_texts(&requests[3], "user");
let (after_resume_last, after_resume_prefix) = after_resume_user_texts
@@ -317,9 +315,8 @@ async fn compact_resume_and_fork_preserve_model_history_view() {
}
let after_fork_user_texts = json_message_input_texts(&requests[4], "user");
let mut expected_after_fork_history_prefix =
vec!["hello world".to_string(), summary_after_fork];
expected_after_fork_history_prefix.extend_from_slice(seeded_user_prefix);
let mut expected_after_fork_history_prefix = seeded_user_prefix.to_vec();
expected_after_fork_history_prefix.extend(["hello world".to_string(), summary_after_fork]);
expected_after_fork_history_prefix.push("AFTER_COMPACT".to_string());
let (after_fork_last, after_fork_prefix) = after_fork_user_texts
.split_last()
@@ -450,15 +447,19 @@ async fn compact_resume_after_second_compaction_preserves_history() -> Result<()
"hello world".to_string(),
"AFTER_COMPACT".to_string(),
"AFTER_RESUME".to_string(),
"AFTER_FORK".to_string(),
summary_after_second_compact.clone(),
];
expected_after_second_compact_user_texts.extend_from_slice(seeded_user_prefix);
expected_after_second_compact_user_texts.push("AFTER_COMPACT_2".to_string());
let mut expected_fork_local_user_texts =
vec!["AFTER_FORK".to_string(), summary_after_second_compact];
expected_fork_local_user_texts.extend_from_slice(seeded_user_prefix);
expected_fork_local_user_texts.push("AFTER_COMPACT_2".to_string());
expected_after_second_compact_user_texts.extend([
"AFTER_FORK".to_string(),
summary_after_second_compact.clone(),
"AFTER_COMPACT_2".to_string(),
]);
let mut expected_fork_local_user_texts = seeded_user_prefix.to_vec();
expected_fork_local_user_texts.extend([
"AFTER_FORK".to_string(),
summary_after_second_compact,
"AFTER_COMPACT_2".to_string(),
]);
let final_user_texts = json_message_input_texts(&requests[requests.len() - 1], "user");
let (final_last, final_prefix) = final_user_texts
.split_last()
+4 -13
View File
@@ -3236,17 +3236,8 @@ async fn production_turn_keeps_rebalanced_catalogs_stable_after_compaction_and_r
_ => None,
})
.expect("compaction retains an allocation checkpoint");
let allocation = checkpoint.state["cloud_skills"]["allocation"]
.as_object()
.expect("retained allocation");
assert_eq!(
serde_json::to_value(checkpoint)?,
json!({
"full": true,
"state": { "cloud_skills": { "allocation": allocation } },
}),
"compaction retains only allocation metadata, not rendered catalogs",
);
assert!(checkpoint.full);
assert!(checkpoint.state["cloud_skills"]["allocation"].is_object());
let resumed = test
.thread_manager
.start_thread(StartThreadOptions {
@@ -3337,7 +3328,7 @@ async fn production_turn_keeps_rebalanced_catalogs_stable_after_compaction_and_r
.flat_map(|text| text.lines())
.filter(|line| line.starts_with("- exec-"))
.collect::<Vec<_>>(),
if matches!(index, 0 | 4) {
if index == 0 {
Vec::new()
} else {
ready_executor_lines.clone()
@@ -3358,7 +3349,7 @@ async fn production_turn_keeps_rebalanced_catalogs_stable_after_compaction_and_r
"cloud_skills_across_executor_readiness"
},
context_snapshot::format_request_history_snapshot(
"Cloud skills rebalance once to retain every executor skill. Post-turn compaction and resume restore the same cloud allocation before the executor reconnects; both catalogs are fully reinjected once into the new history.",
"Cloud skills rebalance once to retain every executor skill. Post-turn compaction installs both catalogs and their allocation before resume; the saved catalogs remain visible while the executor reconnects.",
&requests,
&ContextSnapshotOptions::default()
.rewrite_known_segments()
@@ -26,31 +26,31 @@ Scenario: Manual /compact with prior user history compacts existing history and
04:message/user:
<SUMMARIZATION_PROMPT>
## Window 3 (after request 2: input diverged at item 00, settings changed (parallel_tool_calls, tools))
## Window 3 (after request 2: input diverged at item 03, settings changed (parallel_tool_calls, tools))
-- request 3 (turn) --
00:message/user:
first manual turn
00:message/developer:
<PERMISSIONS_INSTRUCTIONS>
01:message/user:
<ENVIRONMENT_CONTEXT>
02:message/user:
first manual turn
03:message/user:
<COMPACTION_SUMMARY>
FIRST_MANUAL_SUMMARY
02:message/developer:
<PERMISSIONS_INSTRUCTIONS>
03:message/user:
<ENVIRONMENT_CONTEXT>
04:message/user:
second manual turn
## Window 4 (after request 3: settings changed (parallel_tool_calls, tools))
-- request 4 (compaction) --
00:message/user:
first manual turn
00:message/developer:
<PERMISSIONS_INSTRUCTIONS>
01:message/user:
<ENVIRONMENT_CONTEXT>
02:message/user:
first manual turn
03:message/user:
<COMPACTION_SUMMARY>
FIRST_MANUAL_SUMMARY
02:message/developer:
<PERMISSIONS_INSTRUCTIONS>
03:message/user:
<ENVIRONMENT_CONTEXT>
04:message/user:
second manual turn
05:message/assistant:
@@ -58,18 +58,18 @@ Scenario: Manual /compact with prior user history compacts existing history and
06:message/user:
<SUMMARIZATION_PROMPT>
## Window 5 (after request 4: input diverged at item 01, settings changed (parallel_tool_calls, tools))
## Window 5 (after request 4: input diverged at item 00, settings changed (parallel_tool_calls, tools))
-- request 5 (turn) --
00:message/user:
first manual turn
01:message/user:
second manual turn
01:message/developer:
<PERMISSIONS_INSTRUCTIONS>
02:message/user:
<ENVIRONMENT_CONTEXT>
03:message/user:
second manual turn
04:message/user:
<COMPACTION_SUMMARY>
SECOND_MANUAL_SUMMARY
03:message/developer:
<PERMISSIONS_INSTRUCTIONS>
04:message/user:
<ENVIRONMENT_CONTEXT>
05:message/user:
post compact follow-up
@@ -11,12 +11,12 @@ Scenario: Manual /compact with no prior user turn currently still issues a compa
## Window 2 (after request 1: input diverged at item 00, settings changed (parallel_tool_calls, tools))
-- request 2 (turn; Local Post-Compaction History Layout) --
00:message/user:
00:message/developer:
<PERMISSIONS_INSTRUCTIONS>
01:message/user:
<ENVIRONMENT_CONTEXT>
02:message/user:
<COMPACTION_SUMMARY>
MANUAL_EMPTY_SUMMARY
01:message/developer:
<PERMISSIONS_INSTRUCTIONS>
02:message/user:
<ENVIRONMENT_CONTEXT>
03:message/user:
AFTER_MANUAL_EMPTY_COMPACT
@@ -28,15 +28,15 @@ Scenario: Pre-sampling compaction on model switch to a smaller context window: c
## Window 3 (after request 2: input diverged at item 00, settings changed (model, parallel_tool_calls, tools))
-- request 3 (turn; Post-Compaction Follow-up Request (Next Model)) --
00:message/user:
before switch
01:message/user:
<COMPACTION_SUMMARY>
PRE_SAMPLING_SUMMARY
02:message/developer[2]:
00:message/developer[2]:
[01] <MODEL_SWITCH>
[02] <PERMISSIONS_INSTRUCTIONS>
03:message/user:
01:message/user:
<ENVIRONMENT_CONTEXT>
02:message/user:
before switch
03:message/user:
<COMPACTION_SUMMARY>
PRE_SAMPLING_SUMMARY
04:message/user:
after switch
@@ -39,15 +39,15 @@ Scenario: Pre-turn auto-compaction uses the prior context without the incoming u
-- request 4 (turn) --
00:message/user:
USER_ONE
01:message/user:
USER_TWO
01:message/developer:
<PERMISSIONS_INSTRUCTIONS>
02:message/user:
<ENVIRONMENT_CONTEXT>
03:message/user:
USER_TWO
04:message/user:
<COMPACTION_SUMMARY>
PRE_TURN_SUMMARY
03:message/developer:
<PERMISSIONS_INSTRUCTIONS>
04:message/user:
<ENVIRONMENT_CONTEXT>
05:message/user[2]:
[01] <input_image:detail,image_url>
[02] USER_THREE
@@ -28,15 +28,15 @@ Scenario: Pre-turn compaction during model switch (without pre-sampling model-sw
## Window 3 (after request 2: input diverged at item 00, settings changed (parallel_tool_calls, tools))
-- request 3 (turn; Local Post-Compaction History Layout) --
00:message/user:
BEFORE_SWITCH_USER
01:message/user:
<COMPACTION_SUMMARY>
PRETURN_SWITCH_SUMMARY
02:message/developer[2]:
00:message/developer[2]:
[01] <MODEL_SWITCH>
[02] <PERMISSIONS_INSTRUCTIONS>
03:message/user:
01:message/user:
<ENVIRONMENT_CONTEXT>
02:message/user:
BEFORE_SWITCH_USER
03:message/user:
<COMPACTION_SUMMARY>
PRETURN_SWITCH_SUMMARY
04:message/user:
AFTER_SWITCH_USER
@@ -222,10 +222,7 @@ Settings: same as window 1
02:message/user:
Plan a team kickoff for Friday using $outline and $calendar:agenda.
03:message/user:
Summarize the kickoff notes with $notes:summarize.
04:compaction:encrypted=true; chars=26; hash=ec5e2724e6f5f641
05:message/developer[3]:
03:message/developer[3]:
[01] <skills_instructions>
## Skills
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list ...ed into an absolute path using the skill roots table. [hash=ca27c3957f42502b]
@@ -271,7 +268,7 @@ Settings: same as window 1
Never use the `request_user_input` tool for permission requests or permission-related escalations.
</collaboration_mode>
06:message/developer:
04:message/developer:
<multi_agent_role>You are `/root`, the primary agent in a team of agents collaborating to fulfill the user's goals.
At the start of your turn, you are the active agent.
@@ -289,9 +286,9 @@ Settings: same as window 1
There are 4 available concurrency slots, meaning that up to 4 agents can be active at once, including you.
Full-history forks (`fork_turns` omitted or `"all"`) inherit the parent model and reasoning effort and d...ne"` or a positive integer string.</multi_agent_role> [hash=c7f52e90688299ce]
07:message/developer:
05:message/developer:
<multi_agent_mode>Any earlier instruction enabling proactive multi-agent delegation no longer applies. D...elegation, or parallel agent work.</multi_agent_mode> [hash=2b6e037067b4390c]
08:message/user[2]:
06:message/user[2]:
[01] # AGENTS.md instructions for <AGENTS_DIRECTORY>
<INSTRUCTIONS>
@@ -304,6 +301,9 @@ Settings: same as window 1
<timezone><HOST_TIMEZONE></timezone>
<filesystem><workspace_roots><root><WORKSPACE_ROOT 1></root></workspace_roots><permission_profile type...ntry></file_system></permission_profile></filesystem> [hash=911722cc43b4c3b8]
</environment_context>
07:message/user:
Summarize the kickoff notes with $notes:summarize.
08:compaction:encrypted=true; chars=26; hash=ec5e2724e6f5f641
09:message/user[2]:
[01] Check the final kickoff brief and attached sketch with $final-check and $calendar:agenda.
[02] <input_image:image_url>
@@ -108,9 +108,58 @@ Settings:
## Window 2 (after request 2: input diverged at item 00)
Settings: unavailable (items only)
-- request 3 (items; Stored replacement before next turn) --
00:message/user:
00:message/developer[2]:
[01] <permissions instructions>
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`...ll commands are permitted. Network access is enabled. [hash=c134061cebb94a1b]
Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected.
</permissions instructions>
[02] <collaboration_mode># Collaboration Mode: Default
You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active.
Your active mode changes only when new developer instructions with a different `<collaboration_mode>...<...by themselves. Known mode names are Default and Plan. [hash=e75f6b12fc52d812]
## request_user_input availability
Use the `request_user_input` tool only when it is listed in the available tools for this turn.
Use the `request_user_input` tool only for optional questions where the answer would materially improve the quality of the work.
If `request_user_input` returns no answers, continue with best judgment instead of asking again or treating the turn as blocked.
Never use the `request_user_input` tool for permission requests or permission-related escalations.
</collaboration_mode>
01:message/developer:
<multi_agent_role>You are `/root`, the primary agent in a team of agents collaborating to fulfill the user's goals.
At the start of your turn, you are the active agent.
You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents.
All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools.
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task an...message to a running agent without triggering a turn. [hash=7d40ff8b58977202]
`send_message` calls may be read by a human, so ensure they are legible. Always put proper spaces between words and/or numbers.
<OMITTED 17 LINES; ~246 TOKENS; hash=780031e2df907944>
- All agents use the same current working directory.
- As a result, edits made by one agent are immediately visible to all other agents.
When calling `wait_agent`, prefer longer waits (minutes) to avoid busy polling.
There are 4 available concurrency slots, meaning that up to 4 agents can be active at once, including you.
Full-history forks (`fork_turns` omitted or `"all"`) inherit the parent model and reasoning effort and d...ne"` or a positive integer string.</multi_agent_role> [hash=c7f52e90688299ce]
02:message/developer:
<multi_agent_mode>Any earlier instruction enabling proactive multi-agent delegation no longer applies. D...elegation, or parallel agent work.</multi_agent_mode> [hash=2b6e037067b4390c]
03:message/user:
<environment_context>
<cwd><CWD></cwd>
<shell><HOST_SHELL></shell>
<current_date><CURRENT_DATE></current_date>
<timezone><HOST_TIMEZONE></timezone>
<filesystem><workspace_roots><root><CWD></root></workspace_roots><permission_profile type="disabled"><...e="unrestricted" /></permission_profile></filesystem> [hash=d40b2f45cb028888]
</environment_context>
04:message/user:
Plan the release.
01:compaction:encrypted=true; chars=31; hash=8255caf138edede8
05:compaction:encrypted=true; chars=31; hash=8255caf138edede8
## Window 3 (after request 3: input diverged at item 00)
Settings: same as window 1
@@ -148,10 +197,7 @@ Settings: same as window 1
- Relevance: Determine what a plugin can help with from explicit user mention or from the plugin-associa..., MCP tools, and apps exposed elsewhere in this turn. [hash=620459a4e7b590f0]
- Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for t..., say so briefly and continue with the best fallback. [hash=746b0469a8f6cb3e]
02:message/user:
Plan the release.
03:compaction:encrypted=true; chars=31; hash=8255caf138edede8
04:message/developer[2]:
02:message/developer[2]:
[01] <permissions instructions>
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`...ll commands are permitted. Network access is enabled. [hash=c134061cebb94a1b]
Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected.
@@ -172,7 +218,7 @@ Settings: same as window 1
Never use the `request_user_input` tool for permission requests or permission-related escalations.
</collaboration_mode>
05:message/developer:
03:message/developer:
<multi_agent_role>You are `/root`, the primary agent in a team of agents collaborating to fulfill the user's goals.
At the start of your turn, you are the active agent.
@@ -190,9 +236,9 @@ Settings: same as window 1
There are 4 available concurrency slots, meaning that up to 4 agents can be active at once, including you.
Full-history forks (`fork_turns` omitted or `"all"`) inherit the parent model and reasoning effort and d...ne"` or a positive integer string.</multi_agent_role> [hash=c7f52e90688299ce]
06:message/developer:
04:message/developer:
<multi_agent_mode>Any earlier instruction enabling proactive multi-agent delegation no longer applies. D...elegation, or parallel agent work.</multi_agent_mode> [hash=2b6e037067b4390c]
07:message/user:
05:message/user:
<environment_context>
<cwd><CWD></cwd>
<shell><HOST_SHELL></shell>
@@ -200,5 +246,8 @@ Settings: same as window 1
<timezone><HOST_TIMEZONE></timezone>
<filesystem><workspace_roots><root><CWD></root></workspace_roots><permission_profile type="disabled"><...e="unrestricted" /></permission_profile></filesystem> [hash=d40b2f45cb028888]
</environment_context>
06:message/user:
Plan the release.
07:compaction:encrypted=true; chars=31; hash=8255caf138edede8
08:message/user:
Continue with the first step.
@@ -59,14 +59,14 @@ Settings: relative to window 1
<MODEL_INSTRUCTIONS>
02:message/user:
Keep the working tree unchanged.
03:message/user:
Confirm where publishing is allowed.
04:compaction:encrypted=true; chars=39; hash=4010be12538dbedf
05:message/developer[2]:
03:message/developer[2]:
[01] <MODEL_SWITCH>
[02] <PERMISSIONS_INSTRUCTIONS>
06:message/user:
04:message/user:
<ENVIRONMENT_CONTEXT>
05:message/user:
Confirm where publishing is allowed.
06:compaction:encrypted=true; chars=39; hash=4010be12538dbedf
07:message/user:
Continue with the new model.
-- request 5 (turn) --
@@ -196,14 +196,14 @@ Settings: same as window 2
<MODEL_INSTRUCTIONS>
02:message/user:
Keep the working tree unchanged.
03:message/user:
Confirm where publishing is allowed.
04:compaction:encrypted=true; chars=39; hash=4010be12538dbedf
05:message/developer[2]:
03:message/developer[2]:
[01] <MODEL_SWITCH>
[02] <PERMISSIONS_INSTRUCTIONS>
06:message/user:
04:message/user:
<ENVIRONMENT_CONTEXT>
05:message/user:
Confirm where publishing is allowed.
06:compaction:encrypted=true; chars=39; hash=4010be12538dbedf
07:message/user:
Continue with the new model.
08:message/user:
@@ -213,7 +213,7 @@ Settings: same as window 2
-- request 8 (compaction) --
11:compaction_trigger
## Window 5 (after request 8: input diverged at item 04)
## Window 5 (after request 8: input diverged at item 03)
Settings: same as window 2
-- request 9 (turn) --
00:additional_tools/developer (1; hash=1ea3c73f7e3c8b33):
@@ -229,13 +229,13 @@ Settings: same as window 2
Confirm where publishing is allowed.
04:message/user:
Continue with the new model.
05:message/user:
Run another check after compaction.
06:compaction:encrypted=true; chars=33; hash=5c07b4383c427607
07:message/developer:
05:message/developer:
<PERMISSIONS_INSTRUCTIONS>
08:message/user:
06:message/user:
<ENVIRONMENT_CONTEXT>
07:message/user:
Run another check after compaction.
08:compaction:encrypted=true; chars=33; hash=5c07b4383c427607
09:message/user:
Run another check after compaction.
@@ -353,13 +353,13 @@ Settings: same as window 2
Confirm where publishing is allowed.
04:message/user:
Continue with the new model.
05:message/user:
Run another check after compaction.
06:compaction:encrypted=true; chars=33; hash=5c07b4383c427607
07:message/developer:
05:message/developer:
<PERMISSIONS_INSTRUCTIONS>
08:message/user:
06:message/user:
<ENVIRONMENT_CONTEXT>
07:message/user:
Run another check after compaction.
08:compaction:encrypted=true; chars=33; hash=5c07b4383c427607
09:message/user:
Run another check after compaction.
10:function_call/exec_command:{"cmd":"exit 0","sandbox_permissions":"require_escalated"}
@@ -1,8 +1,8 @@
---
source: core/tests/suite/skills_extension.rs
expression: "context_snapshot::format_request_history_snapshot(\"Cloud skills rebalance once to retain every executor skill. Post-turn compaction and resume restore the same cloud allocation before the executor reconnects; both catalogs are fully reinjected once into the new history.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)"
expression: "context_snapshot::format_request_history_snapshot(\"Cloud skills rebalance once to retain every executor skill. Post-turn compaction installs both catalogs and their allocation before resume; the saved catalogs remain visible while the executor reconnects.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)"
---
Scenario: Cloud skills rebalance once to retain every executor skill. Post-turn compaction and resume restore the same cloud allocation before the executor reconnects; both catalogs are fully reinjected once into the new history.
Scenario: Cloud skills rebalance once to retain every executor skill. Post-turn compaction installs both catalogs and their allocation before resume; the saved catalogs remain visible while the executor reconnects.
## Window 1
Settings:
@@ -102,12 +102,7 @@ Settings: same as window 1
-- request 5 (turn) --
00:message/user:
Inspect the available skills.
01:message/user:
Inspect the available skills again.
02:message/user:
<COMPACTION_SUMMARY>
The available skills were inspected.
03:message/developer[3]:
01:message/developer[4]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <environments_instructions>
## Execution environments
@@ -118,14 +113,24 @@ Settings: same as window 1
Wait only when the current task needs that environment. Continue using tools that are already available for unrelated work.
</environments_instructions>
[03] <SKILLS_INSTRUCTIONS>
04:message/user:
[04] <SKILLS_INSTRUCTIONS>
02:message/user:
<ENVIRONMENT_CONTEXT>
03:message/user:
Inspect the available skills again.
04:message/user:
<COMPACTION_SUMMARY>
The available skills were inspected.
05:message/user:
<ENVIRONMENT_CONTEXT>
06:message/developer:
<SKILLS_INSTRUCTIONS>
07:message/user:
Inspect the skills after resuming.
-- request 6 (turn) --
06:function_call/wait_for_environment:{"environment_id":"skills-executor"}
07:function_call_output:{"environment_id":"skills-executor","status":"ready"}
08:message/user:
08:function_call/wait_for_environment:{"environment_id":"skills-executor"}
09:function_call_output:{"environment_id":"skills-executor","status":"ready"}
10:message/user:
<ENVIRONMENT_CONTEXT>
09:message/developer:
11:message/developer:
<SKILLS_INSTRUCTIONS>
@@ -1,8 +1,8 @@
---
source: core/tests/suite/skills_extension.rs
expression: "context_snapshot::format_request_history_snapshot(\"Cloud skills rebalance once to retain every executor skill. Post-turn compaction and resume restore the same cloud allocation before the executor reconnects; both catalogs are fully reinjected once into the new history.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)"
expression: "context_snapshot::format_request_history_snapshot(\"Cloud skills rebalance once to retain every executor skill. Post-turn compaction installs both catalogs and their allocation before resume; the saved catalogs remain visible while the executor reconnects.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)"
---
Scenario: Cloud skills rebalance once to retain every executor skill. Post-turn compaction and resume restore the same cloud allocation before the executor reconnects; both catalogs are fully reinjected once into the new history.
Scenario: Cloud skills rebalance once to retain every executor skill. Post-turn compaction installs both catalogs and their allocation before resume; the saved catalogs remain visible while the executor reconnects.
## Window 1
Settings:
@@ -122,12 +122,7 @@ Settings: same as window 1
-- request 5 (turn) --
00:message/user:
Inspect the available skills.
01:message/user:
Inspect the available skills again.
02:message/user:
<COMPACTION_SUMMARY>
The available skills were inspected.
03:message/developer[3]:
01:message/developer[4]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <environments_instructions>
## Execution environments
@@ -138,9 +133,19 @@ Settings: same as window 1
Wait only when the current task needs that environment. Continue using tools that are already available for unrelated work.
</environments_instructions>
[03] <SKILLS_INSTRUCTIONS>
04:message/user:
[04] <SKILLS_INSTRUCTIONS>
02:message/user:
<ENVIRONMENT_CONTEXT>
03:message/user:
Inspect the available skills again.
04:message/user:
<COMPACTION_SUMMARY>
The available skills were inspected.
05:message/user:
<ENVIRONMENT_CONTEXT>
06:message/developer:
<SKILLS_INSTRUCTIONS>
07:message/user:
Inspect the skills after resuming.
## Window 5 (after request 5)
@@ -148,12 +153,7 @@ Settings: same as window 2
-- request 6 (turn) --
00:message/user:
Inspect the available skills.
01:message/user:
Inspect the available skills again.
02:message/user:
<COMPACTION_SUMMARY>
The available skills were inspected.
03:message/developer[3]:
01:message/developer[4]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <environments_instructions>
## Execution environments
@@ -164,13 +164,23 @@ Settings: same as window 2
Wait only when the current task needs that environment. Continue using tools that are already available for unrelated work.
</environments_instructions>
[03] <SKILLS_INSTRUCTIONS>
[04] <SKILLS_INSTRUCTIONS>
02:message/user:
<ENVIRONMENT_CONTEXT>
03:message/user:
Inspect the available skills again.
04:message/user:
<ENVIRONMENT_CONTEXT>
<COMPACTION_SUMMARY>
The available skills were inspected.
05:message/user:
Inspect the skills after resuming.
06:function_call/wait_for_environment:{"environment_id":"skills-executor"}
07:function_call_output:{"environment_id":"skills-executor","status":"ready"}
08:message/user:
<ENVIRONMENT_CONTEXT>
09:message/developer:
06:message/developer:
<SKILLS_INSTRUCTIONS>
07:message/user:
Inspect the skills after resuming.
08:function_call/wait_for_environment:{"environment_id":"skills-executor"}
09:function_call_output:{"environment_id":"skills-executor","status":"ready"}
10:message/user:
<ENVIRONMENT_CONTEXT>
11:message/developer:
<SKILLS_INSTRUCTIONS>
@@ -142,18 +142,6 @@ pub trait ContextContributor: Send + Sync {
Vec::new()
})
}
/// Retains bounded extension metadata when compaction discards rendered context.
///
/// Return only this contributor's section IDs, without rendered text or availability
/// state. Core persists these partial sections so the next step can rebuild full
/// context without losing decisions that are independent of model-visible history.
fn retain_world_state_after_compaction(
&self,
_previous_world_state: &serde_json::Map<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
serde_json::Map::new()
}
}
/// Contributor for host-owned thread lifecycle gates.
-20
View File
@@ -71,7 +71,6 @@ use crate::tools::SkillAnalytics;
use crate::tools::SkillToolAuthority;
use crate::tools::skill_tools;
use crate::warnings::bounded_warnings;
use crate::world_state::CLOUD_SKILLS_WORLD_STATE_ID;
use crate::world_state_catalogs::CatalogContext;
use crate::world_state_catalogs::CatalogStatus;
@@ -265,25 +264,6 @@ where
.collect()
})
}
fn retain_world_state_after_compaction(
&self,
previous_world_state: &serde_json::Map<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
let mut retained = serde_json::Map::new();
if let Some(allocation) = previous_world_state
.get(CLOUD_SKILLS_WORLD_STATE_ID)
.and_then(|section| section.get("allocation"))
{
// Allocation survives lost history; the catalog fingerprint and budget still
// determine whether it can be reused after fresh discovery and policy resolution.
retained.insert(
CLOUD_SKILLS_WORLD_STATE_ID.to_string(),
serde_json::json!({ "allocation": allocation }),
);
}
retained
}
}
impl<C> ToolContributor for SkillsExtension<C>