Skip to content
Merged
1 change: 1 addition & 0 deletions codex-rs/app-server/tests/suite/send_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@ fn append_rollout_turn_context(path: &Path, timestamp: &str, model: &str) -> std
model: model.to_string(),
personality: None,
collaboration_mode: None,
realtime_active: Some(false),
effort: None,
summary: ReasoningSummary::Auto,
user_instructions: None,
Expand Down
430 changes: 327 additions & 103 deletions codex-rs/core/src/codex.rs

Large diffs are not rendered by default.

27 changes: 15 additions & 12 deletions codex-rs/core/src/codex/rollout_reconstruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use super::*;
#[derive(Debug)]
pub(super) struct RolloutReconstruction {
pub(super) history: Vec<ResponseItem>,
pub(super) previous_model: Option<String>,
pub(super) previous_turn_settings: Option<PreviousTurnSettings>,
pub(super) reference_context_item: Option<TurnContextItem>,
}

Expand All @@ -29,7 +29,7 @@ enum TurnReferenceContextItem {
struct ActiveReplaySegment<'a> {
turn_id: Option<String>,
counts_as_user_turn: bool,
previous_model: Option<String>,
previous_turn_settings: Option<PreviousTurnSettings>,
reference_context_item: TurnReferenceContextItem,
base_replacement_history: Option<&'a [ResponseItem]>,
}
Expand All @@ -42,7 +42,7 @@ fn turn_ids_are_compatible(active_turn_id: Option<&str>, item_turn_id: Option<&s
fn finalize_active_segment<'a>(
active_segment: ActiveReplaySegment<'a>,
base_replacement_history: &mut Option<&'a [ResponseItem]>,
previous_model: &mut Option<String>,
previous_turn_settings: &mut Option<PreviousTurnSettings>,
reference_context_item: &mut TurnReferenceContextItem,
pending_rollback_turns: &mut usize,
) {
Expand All @@ -64,9 +64,9 @@ fn finalize_active_segment<'a>(
*base_replacement_history = Some(segment_base_replacement_history);
}

// `previous_model` comes from the newest surviving user turn that established one.
if previous_model.is_none() && active_segment.counts_as_user_turn {
*previous_model = active_segment.previous_model;
// `previous_turn_settings` come from the newest surviving user turn that established them.
if previous_turn_settings.is_none() && active_segment.counts_as_user_turn {
*previous_turn_settings = active_segment.previous_turn_settings;
}

// `reference_context_item` comes from the newest surviving user turn baseline, or
Expand Down Expand Up @@ -94,7 +94,7 @@ impl Session {
// are both known; then replay only the buffered surviving tail forward to preserve exact
// history semantics.
let mut base_replacement_history: Option<&[ResponseItem]> = None;
let mut previous_model = None;
let mut previous_turn_settings = None;
let mut reference_context_item = TurnReferenceContextItem::NeverSet;
// Rollback is "drop the newest N user turns". While scanning in reverse, that becomes
// "skip the next N user-turn segments we finalize".
Expand Down Expand Up @@ -170,7 +170,10 @@ impl Session {
active_segment.turn_id.as_deref(),
ctx.turn_id.as_deref(),
) {
active_segment.previous_model = Some(ctx.model.clone());
active_segment.previous_turn_settings = Some(PreviousTurnSettings {
model: ctx.model.clone(),
realtime_active: ctx.realtime_active,
});
if matches!(
active_segment.reference_context_item,
TurnReferenceContextItem::NeverSet
Expand All @@ -192,7 +195,7 @@ impl Session {
finalize_active_segment(
active_segment,
&mut base_replacement_history,
&mut previous_model,
&mut previous_turn_settings,
&mut reference_context_item,
&mut pending_rollback_turns,
);
Expand All @@ -204,7 +207,7 @@ impl Session {
}

if base_replacement_history.is_some()
&& previous_model.is_some()
&& previous_turn_settings.is_some()
&& !matches!(reference_context_item, TurnReferenceContextItem::NeverSet)
{
// At this point we have both eager resume metadata values and the replacement-
Expand All @@ -218,7 +221,7 @@ impl Session {
finalize_active_segment(
active_segment,
&mut base_replacement_history,
&mut previous_model,
&mut previous_turn_settings,
&mut reference_context_item,
&mut pending_rollback_turns,
);
Expand Down Expand Up @@ -287,7 +290,7 @@ impl Session {

RolloutReconstruction {
history: history.raw_items().to_vec(),
previous_model,
previous_turn_settings,
reference_context_item,
}
}
Expand Down
98 changes: 70 additions & 28 deletions codex-rs/core/src/codex/rollout_reconstruction_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ fn assistant_message(text: &str) -> ResponseItem {
}

#[tokio::test]
async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previous_model() {
async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previous_turn_settings()
{
let (session, turn_context) = make_session_and_context().await;
let previous_model = "previous-rollout-model";
let previous_context_item = TurnContextItem {
Expand All @@ -48,6 +49,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ
model: previous_model.to_string(),
personality: turn_context.personality,
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort,
summary: turn_context.reasoning_summary,
user_instructions: None,
Expand All @@ -65,12 +67,12 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ
}))
.await;

assert_eq!(session.previous_model().await, None);
assert_eq!(session.previous_turn_settings().await, None);
assert!(session.reference_context_item().await.is_none());
}

#[tokio::test]
async fn record_initial_history_resumed_hydrates_previous_model_from_lifecycle_turn_with_missing_turn_context_id()
async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lifecycle_turn_with_missing_turn_context_id()
{
let (session, turn_context) = make_session_and_context().await;
let previous_model = "previous-rollout-model";
Expand All @@ -85,6 +87,7 @@ async fn record_initial_history_resumed_hydrates_previous_model_from_lifecycle_t
model: previous_model.to_string(),
personality: turn_context.personality,
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort,
summary: turn_context.reasoning_summary,
user_instructions: None,
Expand Down Expand Up @@ -132,8 +135,11 @@ async fn record_initial_history_resumed_hydrates_previous_model_from_lifecycle_t
.await;

assert_eq!(
session.previous_model().await,
Some(previous_model.to_string())
session.previous_turn_settings().await,
Some(PreviousTurnSettings {
model: previous_model.to_string(),
realtime_active: Some(turn_context.realtime_active),
})
);
}

Expand Down Expand Up @@ -220,8 +226,11 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com
vec![turn_one_user, turn_one_assistant]
);
assert_eq!(
reconstructed.previous_model,
Some(turn_context.model_info.slug.clone())
reconstructed.previous_turn_settings,
Some(PreviousTurnSettings {
model: turn_context.model_info.slug.clone(),
realtime_active: Some(turn_context.realtime_active),
})
);
assert_eq!(
serde_json::to_value(reconstructed.reference_context_item)
Expand Down Expand Up @@ -299,8 +308,11 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_inc
vec![turn_one_user, turn_one_assistant]
);
assert_eq!(
reconstructed.previous_model,
Some(turn_context.model_info.slug.clone())
reconstructed.previous_turn_settings,
Some(PreviousTurnSettings {
model: turn_context.model_info.slug.clone(),
realtime_active: Some(turn_context.realtime_active),
})
);
assert_eq!(
serde_json::to_value(reconstructed.reference_context_item)
Expand Down Expand Up @@ -402,8 +414,11 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad
vec![turn_one_user, turn_one_assistant]
);
assert_eq!(
reconstructed.previous_model,
Some(turn_context.model_info.slug.clone())
reconstructed.previous_turn_settings,
Some(PreviousTurnSettings {
model: turn_context.model_info.slug.clone(),
realtime_active: Some(turn_context.realtime_active),
})
);
assert_eq!(
serde_json::to_value(reconstructed.reference_context_item)
Expand Down Expand Up @@ -456,7 +471,7 @@ async fn reconstruct_history_rollback_clears_history_and_metadata_when_exceeding
.await;

assert_eq!(reconstructed.history, Vec::new());
assert_eq!(reconstructed.previous_model, None);
assert_eq!(reconstructed.previous_turn_settings, None);
assert!(reconstructed.reference_context_item.is_none());
}

Expand Down Expand Up @@ -519,7 +534,7 @@ async fn record_initial_history_resumed_rollback_skips_only_user_turns() {
}))
.await;

assert_eq!(session.previous_model().await, None);
assert_eq!(session.previous_turn_settings().await, None);
assert!(session.reference_context_item().await.is_none());
}

Expand Down Expand Up @@ -589,8 +604,11 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp
.await;

assert_eq!(
session.previous_model().await,
Some(turn_context.model_info.slug.clone())
session.previous_turn_settings().await,
Some(PreviousTurnSettings {
model: turn_context.model_info.slug.clone(),
realtime_active: Some(turn_context.realtime_active),
})
);
assert_eq!(
serde_json::to_value(session.reference_context_item().await)
Expand Down Expand Up @@ -637,7 +655,7 @@ async fn record_initial_history_resumed_does_not_seed_reference_context_item_aft
}))
.await;

assert_eq!(session.previous_model().await, None);
assert_eq!(session.previous_turn_settings().await, None);
assert!(session.reference_context_item().await.is_none());
}

Expand Down Expand Up @@ -730,6 +748,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
model: previous_model.to_string(),
personality: turn_context.personality,
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort,
summary: turn_context.reasoning_summary,
user_instructions: None,
Expand Down Expand Up @@ -780,8 +799,11 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
.await;

assert_eq!(
session.previous_model().await,
Some(previous_model.to_string())
session.previous_turn_settings().await,
Some(PreviousTurnSettings {
model: previous_model.to_string(),
realtime_active: Some(turn_context.realtime_active),
})
);
assert_eq!(
serde_json::to_value(session.reference_context_item().await)
Expand All @@ -797,6 +819,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
model: previous_model.to_string(),
personality: turn_context.personality,
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort,
summary: turn_context.reasoning_summary,
user_instructions: None,
Expand Down Expand Up @@ -824,6 +847,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
model: previous_model.to_string(),
personality: turn_context.personality,
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort,
summary: turn_context.reasoning_summary,
user_instructions: None,
Expand Down Expand Up @@ -896,8 +920,11 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
.await;

assert_eq!(
session.previous_model().await,
Some(previous_model.to_string())
session.previous_turn_settings().await,
Some(PreviousTurnSettings {
model: previous_model.to_string(),
realtime_active: Some(turn_context.realtime_active),
})
);
assert!(session.reference_context_item().await.is_none());
}
Expand Down Expand Up @@ -925,6 +952,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
model: current_model.to_string(),
personality: turn_context.personality,
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort,
summary: turn_context.reasoning_summary,
user_instructions: None,
Expand Down Expand Up @@ -995,8 +1023,11 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
.await;

assert_eq!(
session.previous_model().await,
Some(current_model.to_string())
session.previous_turn_settings().await,
Some(PreviousTurnSettings {
model: current_model.to_string(),
realtime_active: Some(turn_context.realtime_active),
})
);
assert_eq!(
serde_json::to_value(session.reference_context_item().await)
Expand All @@ -1022,6 +1053,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
model: previous_model.to_string(),
personality: turn_context.personality,
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort,
summary: turn_context.reasoning_summary,
user_instructions: None,
Expand Down Expand Up @@ -1088,8 +1120,11 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
.await;

assert_eq!(
session.previous_model().await,
Some(previous_model.to_string())
session.previous_turn_settings().await,
Some(PreviousTurnSettings {
model: previous_model.to_string(),
realtime_active: Some(turn_context.realtime_active),
})
);
assert!(session.reference_context_item().await.is_none());
}
Expand Down Expand Up @@ -1131,8 +1166,11 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_preserves_turn_
.await;

assert_eq!(
session.previous_model().await,
Some(turn_context.model_info.slug.clone())
session.previous_turn_settings().await,
Some(PreviousTurnSettings {
model: turn_context.model_info.slug.clone(),
realtime_active: Some(turn_context.realtime_active),
})
);
assert_eq!(
serde_json::to_value(session.reference_context_item().await)
Expand All @@ -1158,6 +1196,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
model: previous_model.to_string(),
personality: turn_context.personality,
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort,
summary: turn_context.reasoning_summary,
user_instructions: None,
Expand Down Expand Up @@ -1234,8 +1273,11 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
.await;

assert_eq!(
session.previous_model().await,
Some(previous_model.to_string())
session.previous_turn_settings().await,
Some(PreviousTurnSettings {
model: previous_model.to_string(),
realtime_active: Some(turn_context.realtime_active),
})
);
assert!(session.reference_context_item().await.is_none());
}
Loading
Loading