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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Preserve model and access program pairs during compaction (#48224)
## Why

Compaction using the previous model can inherit the current turn's access program, producing a model/program pair that the server rejects.

## What changed

- Persist `cyber_access_program` in previous-turn settings and restore it during rollout reconstruction.
- Use the previous turn's access program when compacting with its model, preserving an absent program instead of inheriting the current selection.
- Pass the selected access program into local compaction prompts.

## Testing

Add regression coverage for local and remote compaction after model switches, including resume, fork, rollback, and compaction checkpoints. Verify fallback and subsequent sampling use the current model/program pair, and API-key sessions do not inherit access-program authorization.

GitOrigin-RevId: eaa7162e8711446dc9eb1bfff194e711abd54d7e
  • Loading branch information
jamy-OAI authored and copyberry committed Sep 25, 2026
commit 5f3180c79374bd9440db733e23ead8bc9f62d64b
Binary file not shown.
2 changes: 2 additions & 0 deletions codex-rs/core/src/agent/control_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1925,6 +1925,7 @@ async fn spawn_agent_fork_sanitizes_inherited_compaction_metadata() {
previous_turn_settings: Some(codex_history::PreviousTurnSettings {
model: "parent-model".into(),
comp_hash: None,
cyber_access_program: None,
realtime_active: None,
}),
};
Expand Down Expand Up @@ -3011,6 +3012,7 @@ async fn spawn_agent_full_fork_legacy_compaction_rebuilds_child_instructions_onc
previous_turn_settings: Some(codex_history::PreviousTurnSettings {
model: "parent-model".into(),
comp_hash: None,
cyber_access_program: None,
realtime_active: None,
}),
}),
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/compact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ async fn run_compact_task_inner_impl(
let prompt = Prompt {
input: turn_input,
base_instructions: sess.get_prompt_base_instructions().await,
cyber_access_program: turn_context.cyber_access_program,
..Default::default()
};
let responses_metadata = sess
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/session/rollout_reconstruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ impl Session {
) {
active_segment.previous_turn_settings = Some(PreviousTurnSettings {
model: ctx.model.clone(),
cyber_access_program: ctx.cyber_access_program,
comp_hash: ctx.comp_hash.clone(),
realtime_active: ctx.realtime_active,
});
Expand Down
45 changes: 33 additions & 12 deletions codex-rs/core/src/session/rollout_reconstruction_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use codex_protocol::protocol::SessionMetaLine;
use codex_protocol::protocol::ThreadRolledBackEvent;
use codex_protocol::protocol::WorldStateItem;
use codex_protocol::security_risk::SecurityRiskScore;
use codex_protocol::turn_input::CyberAccessProgram;
use codex_rollout::ModelContextScan;
use codex_rollout::ModelContextScanProgress;
use core_test_support::responses::strip_metadata_from_items;
Expand Down Expand Up @@ -404,6 +405,7 @@ async fn record_initial_history_restores_world_state_baseline(input: BaselineTur
Some(PreviousTurnSettings {
model: context_item.model.clone(),
comp_hash: context_item.comp_hash.clone(),
cyber_access_program: None,
realtime_active: context_item.realtime_active,
}),
serde_json::to_value(Some(context_item)).unwrap(),
Expand Down Expand Up @@ -558,6 +560,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
Some(PreviousTurnSettings {
model: previous_model.to_string(),
comp_hash: Some("comp-hash-a".to_string()),
cyber_access_program: None,
realtime_active: Some(turn_context.realtime_active),
})
);
Expand All @@ -566,14 +569,16 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
#[tokio::test]
async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_completed_turns() {
let (session, turn_context) = make_session_and_context().await;
let first_context_item = turn_context.to_turn_context_item();
let mut first_context_item = turn_context.to_turn_context_item();
first_context_item.cyber_access_program = Some(CyberAccessProgram::DaybreakBlue);
let first_turn_id = first_context_item
.turn_id
.clone()
.expect("turn context should have turn_id");
let mut rolled_back_context_item = first_context_item.clone();
rolled_back_context_item.turn_id = Some("rolled-back-turn".to_string());
rolled_back_context_item.model = "rolled-back-model".to_string();
rolled_back_context_item.cyber_access_program = Some(CyberAccessProgram::DaybreakRed);
let rolled_back_turn_id = rolled_back_context_item
.turn_id
.clone()
Expand Down Expand Up @@ -672,11 +677,13 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com
annotated(vec![turn_one_user, turn_one_assistant])
);
assert_eq!(
reconstructed.previous_turn_settings,
Some(PreviousTurnSettings {
model: turn_context.model_info().slug.clone(),
comp_hash: None,
realtime_active: Some(turn_context.realtime_active),
serde_json::to_value(reconstructed.previous_turn_settings)
.expect("serialize previous settings"),
json!({
"model": turn_context.model_info().slug,
"comp_hash": null,
"realtime_active": turn_context.realtime_active,
"cyber_access_program": "daybreak_blue",
})
);
assert_eq!(
Expand All @@ -695,7 +702,8 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com
#[tokio::test]
async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_incomplete_turn() {
let (session, turn_context) = make_session_and_context().await;
let first_context_item = turn_context.to_turn_context_item();
let mut first_context_item = turn_context.to_turn_context_item();
first_context_item.cyber_access_program = Some(CyberAccessProgram::DaybreakBlue);
let first_turn_id = first_context_item
.turn_id
.clone()
Expand Down Expand Up @@ -775,11 +783,13 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_inc
annotated(vec![turn_one_user, turn_one_assistant])
);
assert_eq!(
reconstructed.previous_turn_settings,
Some(PreviousTurnSettings {
model: turn_context.model_info().slug.clone(),
comp_hash: None,
realtime_active: Some(turn_context.realtime_active),
serde_json::to_value(reconstructed.previous_turn_settings)
.expect("serialize previous settings"),
json!({
"model": turn_context.model_info().slug,
"comp_hash": null,
"realtime_active": turn_context.realtime_active,
"cyber_access_program": "daybreak_blue",
})
);
assert_eq!(
Expand Down Expand Up @@ -919,6 +929,7 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad
Some(PreviousTurnSettings {
model: turn_context.model_info().slug.clone(),
comp_hash: None,
cyber_access_program: None,
realtime_active: Some(turn_context.realtime_active),
})
);
Expand Down Expand Up @@ -1026,6 +1037,7 @@ async fn reconstruct_history_rollback_counts_inter_agent_assistant_turns() {
Some(PreviousTurnSettings {
model: turn_context.model_info().slug.clone(),
comp_hash: None,
cyber_access_program: None,
realtime_active: Some(turn_context.realtime_active),
})
);
Expand Down Expand Up @@ -1270,6 +1282,7 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp
Some(PreviousTurnSettings {
model: turn_context.model_info().slug.clone(),
comp_hash: None,
cyber_access_program: None,
realtime_active: Some(turn_context.realtime_active),
})
);
Expand Down Expand Up @@ -1557,6 +1570,7 @@ async fn bounded_replay_matches_full_replay_after_empty_turn_compactions(
previous_turn_settings: Some(PreviousTurnSettings {
model: format!("metadata-model-{window_number}"),
comp_hash: Some(format!("metadata-hash-{window_number}")),
cyber_access_program: None,
realtime_active: Some(false),
}),
}),
Expand Down Expand Up @@ -1672,6 +1686,7 @@ async fn completed_turn_suffix_after_compaction_overrides_resume_metadata() {
let expected_settings = PreviousTurnSettings {
model: newer_context.model.clone(),
comp_hash: newer_context.comp_hash.clone(),
cyber_access_program: None,
realtime_active: newer_context.realtime_active,
};
let mut rollout_items = vec![
Expand Down Expand Up @@ -1978,6 +1993,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
Some(PreviousTurnSettings {
model: previous_model.to_string(),
comp_hash: None,
cyber_access_program: None,
realtime_active: Some(turn_context.realtime_active),
})
);
Expand Down Expand Up @@ -2144,6 +2160,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
Some(PreviousTurnSettings {
model: previous_model.to_string(),
comp_hash: None,
cyber_access_program: None,
realtime_active: Some(turn_context.realtime_active),
})
);
Expand Down Expand Up @@ -2279,6 +2296,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
Some(PreviousTurnSettings {
model: current_model.to_string(),
comp_hash: None,
cyber_access_program: None,
realtime_active: Some(turn_context.realtime_active),
})
);
Expand Down Expand Up @@ -2410,6 +2428,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
Some(PreviousTurnSettings {
model: previous_model.to_string(),
comp_hash: None,
cyber_access_program: None,
realtime_active: Some(turn_context.realtime_active),
})
);
Expand Down Expand Up @@ -2462,6 +2481,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_preserves_turn_
Some(PreviousTurnSettings {
model: turn_context.model_info().slug.clone(),
comp_hash: None,
cyber_access_program: None,
realtime_active: Some(turn_context.realtime_active),
})
);
Expand Down Expand Up @@ -2606,6 +2626,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
Some(PreviousTurnSettings {
model: previous_model.to_string(),
comp_hash: None,
cyber_access_program: None,
realtime_active: Some(turn_context.realtime_active),
})
);
Expand Down
7 changes: 7 additions & 0 deletions codex-rs/core/src/session/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4044,6 +4044,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
Some(PreviousTurnSettings {
model: previous_model.to_string(),
comp_hash: None,
cyber_access_program: None,
realtime_active: Some(turn_context.realtime_active),
})
);
Expand Down Expand Up @@ -5791,6 +5792,7 @@ async fn compaction_persists_resume_metadata_and_companion_records() {
let previous_turn_settings = PreviousTurnSettings {
model: "previous-model".to_string(),
comp_hash: Some("comp-hash".to_string()),
cyber_access_program: None,
realtime_active: Some(true),
};
session
Expand Down Expand Up @@ -10515,6 +10517,7 @@ async fn build_initial_context_restates_realtime_start_when_reference_context_is
let previous_turn_settings = PreviousTurnSettings {
model: turn_context.model_info().slug.clone(),
comp_hash: None,
cyber_access_program: None,
realtime_active: Some(true),
};

Expand Down Expand Up @@ -10878,6 +10881,7 @@ async fn build_initial_context_uses_retained_step_after_model_change() {
.set_previous_turn_settings(Some(PreviousTurnSettings {
model: "base-model".to_string(),
comp_hash: None,
cyber_access_program: None,
realtime_active: None,
}))
.await;
Expand Down Expand Up @@ -10959,6 +10963,7 @@ async fn build_initial_context_prepends_model_switch_message() {
let previous_turn_settings = PreviousTurnSettings {
model: "previous-regular-model".to_string(),
comp_hash: None,
cyber_access_program: None,
realtime_active: None,
};

Expand Down Expand Up @@ -11013,6 +11018,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_full_rei
.set_previous_turn_settings(Some(PreviousTurnSettings {
model: previous_context.model_info().slug.clone(),
comp_hash: None,
cyber_access_program: None,
realtime_active: Some(previous_context.realtime_active),
}))
.await;
Expand Down Expand Up @@ -11582,6 +11588,7 @@ async fn interrupting_compaction_fallback_retains_last_known_step_context() {
.set_previous_turn_settings(Some(PreviousTurnSettings {
model: "gpt-5.4".to_string(),
comp_hash: Some("old".to_string()),
cyber_access_program: None,
realtime_active: Some(turn.realtime_active),
}))
.await;
Expand Down
15 changes: 10 additions & 5 deletions codex-rs/core/src/session/turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ pub(crate) async fn run_turn(
.await;
sess.set_previous_turn_settings(Some(PreviousTurnSettings {
model: turn_context.model_info().slug.clone(),
cyber_access_program: turn_context.cyber_access_program,
comp_hash: turn_context.model_info().comp_hash.clone(),
realtime_active: Some(turn_context.realtime_active),
}))
Expand Down Expand Up @@ -1359,11 +1360,15 @@ async fn maybe_run_previous_model_inline_compact(
if !should_compact_for_comp_hash_change && previous_model == turn_context.model_info().slug {
return Ok(());
}
let previous_model_turn_context = Arc::new(
turn_context
.with_model(previous_model.clone(), &sess.services.models_manager)
.await,
);
let mut previous_model_turn_context = turn_context
.with_model(previous_model.clone(), &sess.services.models_manager)
.await;
// `with_model` preserves the current turn's access program. Restore the previous
// turn's program so compaction uses the same model/cyber_access_program pair as that turn.
// Combining the previous model with the current turn's program can produce a pair
// that the server rejects.
previous_model_turn_context.cyber_access_program = previous_turn_settings.cyber_access_program;
let previous_model_turn_context = Arc::new(previous_model_turn_context);

if should_compact_for_comp_hash_change {
let step_context = sess
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/core/tests/suite/compact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5684,3 +5684,6 @@ async fn remote_v2_compaction_refreshes_instructions_and_preserves_them_on_cold_

Ok(())
}

#[path = "compact_program_tests.rs"]
mod program_tests;
Loading
Loading