use super::*;use super::tests::make_session_and_context;use codex_protocol::AgentPath;use codex_protocol::ThreadId;use codex_protocol::models::ContentItem;use codex_protocol::models::ResponseItem;use codex_protocol::protocol::CompactedItem;use codex_protocol::protocol::InitialHistory;use codex_protocol::protocol::InterAgentCommunication;use codex_protocol::protocol::ResumedHistory;use pretty_assertions::assert_eq;use std::path::PathBuf;fn user_message(text: &str) -> ResponseItem { ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: text.to_string(), }], phase: None, metadata: None, }}fn assistant_message(text: &str) -> ResponseItem { ResponseItem::Message { id: None, role: "assistant".to_string(), content: vec![ContentItem::OutputText { text: text.to_string(), }], phase: None, metadata: None, }}fn inter_agent_assistant_message(text: &str) -> ResponseItem { let communication = InterAgentCommunication::new( AgentPath::root(), AgentPath::root().join("worker").unwrap(), Vec::new(), text.to_string(), /*trigger_turn*/ true, ); ResponseItem::Message { id: None, role: "assistant".to_string(), content: vec![ContentItem::OutputText { text: serde_json::to_string(&communication).unwrap(), }], phase: None, metadata: None, }}#[tokio::test]async fn record_initial_history_reconstructs_typed_inter_agent_message() { let (session, _turn_context) = make_session_and_context().await; let communication = InterAgentCommunication::new( AgentPath::root().join("worker").expect("worker path"), AgentPath::root(), Vec::new(), "child done".to_string(), /*trigger_turn*/ false, ); session .record_initial_history(InitialHistory::Resumed(ResumedHistory { conversation_id: ThreadId::default(), history: vec![RolloutItem::InterAgentCommunication(communication.clone())], rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")), })) .await; assert_eq!( session.state.lock().await.clone_history().raw_items(), &[communication.to_model_input_item()] );}#[tokio::test]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 { turn_id: Some(turn_context.sub_id.clone()), #[allow(deprecated)] cwd: turn_context.cwd.to_path_buf(), workspace_roots: None, current_date: turn_context.current_date.clone(), timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), sandbox_policy: turn_context.sandbox_policy(), permission_profile: None, network: None, file_system_sandbox_policy: None, model: previous_model.to_string(), comp_hash: None, personality: turn_context.personality, collaboration_mode: Some(turn_context.collaboration_mode.clone()), multi_agent_version: None, realtime_active: Some(turn_context.realtime_active), effort: turn_context.reasoning_effort.clone(), summary: codex_protocol::config_types::ReasoningSummary::Auto, }; let rollout_items = vec![RolloutItem::TurnContext(previous_context_item)]; session .record_initial_history(InitialHistory::Resumed(ResumedHistory { conversation_id: ThreadId::default(), history: rollout_items, rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")), })) .await; 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_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"; let mut previous_context_item = TurnContextItem { turn_id: Some(turn_context.sub_id.clone()), #[allow(deprecated)] cwd: turn_context.cwd.to_path_buf(), workspace_roots: None, current_date: turn_context.current_date.clone(), timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), sandbox_policy: turn_context.sandbox_policy(), permission_profile: None, network: None, file_system_sandbox_policy: None, model: previous_model.to_string(), comp_hash: Some("comp-hash-a".to_string()), personality: turn_context.personality, collaboration_mode: Some(turn_context.collaboration_mode.clone()), multi_agent_version: None, realtime_active: Some(turn_context.realtime_active), effort: turn_context.reasoning_effort.clone(), summary: codex_protocol::config_types::ReasoningSummary::Auto, }; let turn_id = previous_context_item .turn_id .clone() .expect("turn context should have turn_id"); previous_context_item.turn_id = None; let rollout_items = vec![ RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: turn_id.clone(), trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "seed".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::TurnContext(previous_context_item), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id, last_agent_message: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, }, )), ]; session .record_initial_history(InitialHistory::Resumed(ResumedHistory { conversation_id: ThreadId::default(), history: rollout_items, rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")), })) .await; assert_eq!( session.previous_turn_settings().await, Some(PreviousTurnSettings { model: previous_model.to_string(), comp_hash: Some("comp-hash-a".to_string()), realtime_active: Some(turn_context.realtime_active), }) );}#[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 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(); let rolled_back_turn_id = rolled_back_context_item .turn_id .clone() .expect("turn context should have turn_id"); let turn_one_user = user_message("turn 1 user"); let turn_one_assistant = assistant_message("turn 1 assistant"); let turn_two_user = user_message("turn 2 user"); let turn_two_assistant = assistant_message("turn 2 assistant"); let rollout_items = vec![ RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: first_turn_id.clone(), trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "turn 1 user".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::TurnContext(first_context_item.clone()), RolloutItem::ResponseItem(turn_one_user.clone()), RolloutItem::ResponseItem(turn_one_assistant.clone()), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: first_turn_id, last_agent_message: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: rolled_back_turn_id.clone(), trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "turn 2 user".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::TurnContext(rolled_back_context_item), RolloutItem::ResponseItem(turn_two_user), RolloutItem::ResponseItem(turn_two_assistant), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: rolled_back_turn_id, last_agent_message: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::ThreadRolledBack( codex_protocol::protocol::ThreadRolledBackEvent { num_turns: 1 }, )), ]; let reconstructed = session .reconstruct_history_from_rollout(&turn_context, &rollout_items) .await; assert_eq!( reconstructed.history, 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), }) ); assert_eq!( serde_json::to_value(reconstructed.reference_context_item) .expect("serialize reconstructed reference context item"), serde_json::to_value(Some(first_context_item)) .expect("serialize expected reference context item") );}#[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 first_turn_id = first_context_item .turn_id .clone() .expect("turn context should have turn_id"); let incomplete_turn_id = "incomplete-rolled-back-turn".to_string(); let turn_one_user = user_message("turn 1 user"); let turn_one_assistant = assistant_message("turn 1 assistant"); let turn_two_user = user_message("turn 2 user"); let rollout_items = vec![ RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: first_turn_id.clone(), trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "turn 1 user".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::TurnContext(first_context_item.clone()), RolloutItem::ResponseItem(turn_one_user.clone()), RolloutItem::ResponseItem(turn_one_assistant.clone()), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: first_turn_id, last_agent_message: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: incomplete_turn_id, trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "turn 2 user".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::ResponseItem(turn_two_user), RolloutItem::EventMsg(EventMsg::ThreadRolledBack( codex_protocol::protocol::ThreadRolledBackEvent { num_turns: 1 }, )), ]; let reconstructed = session .reconstruct_history_from_rollout(&turn_context, &rollout_items) .await; assert_eq!( reconstructed.history, 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), }) ); assert_eq!( serde_json::to_value(reconstructed.reference_context_item) .expect("serialize reconstructed reference context item"), serde_json::to_value(Some(first_context_item)) .expect("serialize expected reference context item") );}#[tokio::test]async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metadata() { let (session, turn_context) = make_session_and_context().await; let first_context_item = turn_context.to_turn_context_item(); let first_turn_id = first_context_item .turn_id .clone() .expect("turn context should have turn_id"); let second_turn_id = "rolled-back-user-turn".to_string(); let standalone_turn_id = "standalone-turn".to_string(); let turn_one_user = user_message("turn 1 user"); let turn_one_assistant = assistant_message("turn 1 assistant"); let turn_two_user = user_message("turn 2 user"); let turn_two_assistant = assistant_message("turn 2 assistant"); let standalone_assistant = assistant_message("standalone assistant"); let rollout_items = vec![ RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: first_turn_id.clone(), trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "turn 1 user".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::TurnContext(first_context_item.clone()), RolloutItem::ResponseItem(turn_one_user.clone()), RolloutItem::ResponseItem(turn_one_assistant.clone()), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: first_turn_id, last_agent_message: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: second_turn_id.clone(), trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "turn 2 user".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::ResponseItem(turn_two_user), RolloutItem::ResponseItem(turn_two_assistant), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: second_turn_id, last_agent_message: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: standalone_turn_id.clone(), trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::ResponseItem(standalone_assistant), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: standalone_turn_id, last_agent_message: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::ThreadRolledBack( codex_protocol::protocol::ThreadRolledBackEvent { num_turns: 1 }, )), ]; let reconstructed = session .reconstruct_history_from_rollout(&turn_context, &rollout_items) .await; assert_eq!( reconstructed.history, 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), }) ); assert_eq!( serde_json::to_value(reconstructed.reference_context_item) .expect("serialize reconstructed reference context item"), serde_json::to_value(Some(first_context_item)) .expect("serialize expected reference context item") );}#[tokio::test]async fn reconstruct_history_rollback_counts_inter_agent_assistant_turns() { let (session, turn_context) = make_session_and_context().await; let first_context_item = turn_context.to_turn_context_item(); let first_turn_id = first_context_item .turn_id .clone() .expect("turn context should have turn_id"); let assistant_turn_id = "assistant-instruction-turn".to_string(); let assistant_turn_context = TurnContextItem { turn_id: Some(assistant_turn_id.clone()), ..first_context_item.clone() }; let assistant_instruction = inter_agent_assistant_message("continue"); let assistant_reply = assistant_message("worker reply"); let rollout_items = vec![ RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: first_turn_id.clone(), trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "turn 1 user".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::TurnContext(first_context_item.clone()), RolloutItem::ResponseItem(user_message("turn 1 user")), RolloutItem::ResponseItem(assistant_message("turn 1 assistant")), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: first_turn_id, last_agent_message: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: assistant_turn_id.clone(), trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::TurnContext(assistant_turn_context), RolloutItem::ResponseItem(assistant_instruction), RolloutItem::ResponseItem(assistant_reply), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: assistant_turn_id, last_agent_message: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::ThreadRolledBack( codex_protocol::protocol::ThreadRolledBackEvent { num_turns: 1 }, )), ]; let reconstructed = session .reconstruct_history_from_rollout(&turn_context, &rollout_items) .await; assert_eq!( reconstructed.history, vec![ user_message("turn 1 user"), assistant_message("turn 1 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), }) ); assert_eq!( serde_json::to_value(reconstructed.reference_context_item) .expect("serialize reconstructed reference context item"), serde_json::to_value(Some(first_context_item)) .expect("serialize expected reference context item") );}#[tokio::test]async fn reconstruct_history_rollback_clears_history_and_metadata_when_exceeding_user_turns() { let (session, turn_context) = make_session_and_context().await; let only_context_item = turn_context.to_turn_context_item(); let only_turn_id = only_context_item .turn_id .clone() .expect("turn context should have turn_id"); let rollout_items = vec![ RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: only_turn_id.clone(), trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "only user".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::TurnContext(only_context_item), RolloutItem::ResponseItem(user_message("only user")), RolloutItem::ResponseItem(assistant_message("only assistant")), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: only_turn_id, last_agent_message: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::ThreadRolledBack( codex_protocol::protocol::ThreadRolledBackEvent { num_turns: 99 }, )), ]; let reconstructed = session .reconstruct_history_from_rollout(&turn_context, &rollout_items) .await; assert_eq!(reconstructed.history, Vec::new()); assert_eq!(reconstructed.previous_turn_settings, None); assert!(reconstructed.reference_context_item.is_none());}#[tokio::test]async fn record_initial_history_resumed_rollback_skips_only_user_turns() { let (session, turn_context) = make_session_and_context().await; let previous_context_item = turn_context.to_turn_context_item(); let user_turn_id = previous_context_item .turn_id .clone() .expect("turn context should have turn_id"); let standalone_turn_id = "standalone-task-turn".to_string(); let rollout_items = vec![ RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: user_turn_id.clone(), trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "seed".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::TurnContext(previous_context_item), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: user_turn_id, last_agent_message: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, }, )), // Standalone task turn (no UserMessage) should not consume rollback skips. RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: standalone_turn_id.clone(), trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: standalone_turn_id, last_agent_message: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::ThreadRolledBack( codex_protocol::protocol::ThreadRolledBackEvent { num_turns: 1 }, )), ]; session .record_initial_history(InitialHistory::Resumed(ResumedHistory { conversation_id: ThreadId::default(), history: rollout_items, rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")), })) .await; assert_eq!(session.previous_turn_settings().await, None); assert!(session.reference_context_item().await.is_none());}#[tokio::test]async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_compaction_metadata() { let (session, turn_context) = make_session_and_context().await; let previous_context_item = turn_context.to_turn_context_item(); let previous_turn_id = previous_context_item .turn_id .clone() .expect("turn context should have turn_id"); let incomplete_turn_id = "incomplete-compacted-user-turn".to_string(); let rollout_items = vec![ RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: previous_turn_id.clone(), trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "seed".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::TurnContext(previous_context_item.clone()), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: previous_turn_id, last_agent_message: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: incomplete_turn_id, trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "rolled back".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::Compacted(CompactedItem { message: String::new(), replacement_history: Some(Vec::new()), window_id: None, }), RolloutItem::EventMsg(EventMsg::ThreadRolledBack( codex_protocol::protocol::ThreadRolledBackEvent { num_turns: 1 }, )), ]; session .record_initial_history(InitialHistory::Resumed(ResumedHistory { conversation_id: ThreadId::default(), history: rollout_items, rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")), })) .await; assert_eq!( session.previous_turn_settings().await, Some(PreviousTurnSettings { model: turn_context.model_info.slug.clone(), comp_hash: None, realtime_active: Some(turn_context.realtime_active), }) ); assert_eq!( serde_json::to_value(session.reference_context_item().await) .expect("serialize seeded reference context item"), serde_json::to_value(Some(previous_context_item)) .expect("serialize expected reference context item") );}#[tokio::test]async fn record_initial_history_resumed_bare_turn_context_does_not_seed_reference_context_item() { let (session, turn_context) = make_session_and_context().await; let previous_context_item = turn_context.to_turn_context_item(); let rollout_items = vec![RolloutItem::TurnContext(previous_context_item.clone())]; session .record_initial_history(InitialHistory::Resumed(ResumedHistory { conversation_id: ThreadId::default(), history: rollout_items, rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")), })) .await; assert!(session.reference_context_item().await.is_none());}#[tokio::test]async fn record_initial_history_resumed_does_not_seed_reference_context_item_after_compaction() { let (session, turn_context) = make_session_and_context().await; let previous_context_item = turn_context.to_turn_context_item(); let rollout_items = vec![ RolloutItem::TurnContext(previous_context_item), RolloutItem::Compacted(CompactedItem { message: String::new(), replacement_history: Some(Vec::new()), window_id: None, }), ]; session .record_initial_history(InitialHistory::Resumed(ResumedHistory { conversation_id: ThreadId::default(), history: rollout_items, rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")), })) .await; assert_eq!(session.previous_turn_settings().await, None); assert!(session.reference_context_item().await.is_none());}#[tokio::test]async fn reconstruct_history_legacy_compaction_without_replacement_history_does_not_inject_current_initial_context() { let (session, turn_context) = make_session_and_context().await; let rollout_items = vec![ RolloutItem::ResponseItem(user_message("before compact")), RolloutItem::ResponseItem(assistant_message("assistant reply")), RolloutItem::Compacted(CompactedItem { message: "legacy summary".to_string(), replacement_history: None, window_id: None, }), ]; let reconstructed = session .reconstruct_history_from_rollout(&turn_context, &rollout_items) .await; assert_eq!( reconstructed.history, vec![ user_message("before compact"), user_message("legacy summary"), ] ); assert!(reconstructed.reference_context_item.is_none());}#[tokio::test]async fn reconstruct_history_legacy_compaction_without_replacement_history_clears_later_reference_context_item() { let (session, turn_context) = make_session_and_context().await; let current_context_item = turn_context.to_turn_context_item(); let current_turn_id = current_context_item .turn_id .clone() .expect("turn context should have turn_id"); let rollout_items = vec![ RolloutItem::ResponseItem(user_message("before compact")), RolloutItem::Compacted(CompactedItem { message: "legacy summary".to_string(), replacement_history: None, window_id: None, }), RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: current_turn_id.clone(), trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "after legacy compact".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::TurnContext(current_context_item), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: current_turn_id, last_agent_message: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, }, )), ]; let reconstructed = session .reconstruct_history_from_rollout(&turn_context, &rollout_items) .await; assert!(reconstructed.reference_context_item.is_none());}#[tokio::test]async fn record_initial_history_resumed_turn_context_after_compaction_reestablishes_reference_context_item() { let (session, turn_context) = make_session_and_context().await; let previous_model = "previous-rollout-model"; let previous_context_item = TurnContextItem { turn_id: Some(turn_context.sub_id.clone()), #[allow(deprecated)] cwd: turn_context.cwd.to_path_buf(), workspace_roots: None, current_date: turn_context.current_date.clone(), timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), sandbox_policy: turn_context.sandbox_policy(), permission_profile: None, network: None, file_system_sandbox_policy: None, model: previous_model.to_string(), comp_hash: None, personality: turn_context.personality, collaboration_mode: Some(turn_context.collaboration_mode.clone()), multi_agent_version: None, realtime_active: Some(turn_context.realtime_active), effort: turn_context.reasoning_effort.clone(), summary: codex_protocol::config_types::ReasoningSummary::Auto, }; let previous_turn_id = previous_context_item .turn_id .clone() .expect("turn context should have turn_id"); let rollout_items = vec![ RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: previous_turn_id.clone(), trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "seed".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), // Compaction clears baseline until a later TurnContextItem re-establishes it. RolloutItem::Compacted(CompactedItem { message: String::new(), replacement_history: Some(Vec::new()), window_id: None, }), RolloutItem::TurnContext(previous_context_item), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: previous_turn_id, last_agent_message: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, }, )), ]; session .record_initial_history(InitialHistory::Resumed(ResumedHistory { conversation_id: ThreadId::default(), history: rollout_items, rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")), })) .await; assert_eq!( session.previous_turn_settings().await, Some(PreviousTurnSettings { model: previous_model.to_string(), comp_hash: None, realtime_active: Some(turn_context.realtime_active), }) ); assert_eq!( serde_json::to_value(session.reference_context_item().await) .expect("serialize seeded reference context item"), serde_json::to_value(Some(TurnContextItem { turn_id: Some(turn_context.sub_id.clone()), #[allow(deprecated)] cwd: turn_context.cwd.to_path_buf(), workspace_roots: None, current_date: turn_context.current_date.clone(), timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), sandbox_policy: turn_context.sandbox_policy(), permission_profile: None, network: None, file_system_sandbox_policy: None, model: previous_model.to_string(), comp_hash: None, personality: turn_context.personality, collaboration_mode: Some(turn_context.collaboration_mode.clone()), multi_agent_version: None, realtime_active: Some(turn_context.realtime_active), effort: turn_context.reasoning_effort.clone(), summary: codex_protocol::config_types::ReasoningSummary::Auto, })) .expect("serialize expected reference context item") );}#[tokio::test]async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_turn_for_compaction_accounting() { let (session, turn_context) = make_session_and_context().await; let previous_model = "previous-rollout-model"; let previous_context_item = TurnContextItem { turn_id: Some(turn_context.sub_id.clone()), #[allow(deprecated)] cwd: turn_context.cwd.to_path_buf(), workspace_roots: None, current_date: turn_context.current_date.clone(), timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), sandbox_policy: turn_context.sandbox_policy(), permission_profile: None, network: None, file_system_sandbox_policy: None, model: previous_model.to_string(), comp_hash: None, personality: turn_context.personality, collaboration_mode: Some(turn_context.collaboration_mode.clone()), multi_agent_version: None, realtime_active: Some(turn_context.realtime_active), effort: turn_context.reasoning_effort.clone(), summary: codex_protocol::config_types::ReasoningSummary::Auto, }; let previous_turn_id = previous_context_item .turn_id .clone() .expect("turn context should have turn_id"); let aborted_turn_id = "aborted-turn-without-id".to_string(); let rollout_items = vec![ RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: previous_turn_id.clone(), trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "seed".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::TurnContext(previous_context_item), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: previous_turn_id, last_agent_message: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: aborted_turn_id, trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "aborted".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::EventMsg(EventMsg::TurnAborted( codex_protocol::protocol::TurnAbortedEvent { turn_id: None, reason: TurnAbortReason::Interrupted, completed_at: None, duration_ms: None, }, )), RolloutItem::Compacted(CompactedItem { message: String::new(), replacement_history: Some(Vec::new()), window_id: None, }), ]; session .record_initial_history(InitialHistory::Resumed(ResumedHistory { conversation_id: ThreadId::default(), history: rollout_items, rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")), })) .await; assert_eq!( session.previous_turn_settings().await, Some(PreviousTurnSettings { model: previous_model.to_string(), comp_hash: None, realtime_active: Some(turn_context.realtime_active), }) ); assert!(session.reference_context_item().await.is_none());}#[tokio::test]async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_for_later_turn_context() { let (session, turn_context) = make_session_and_context().await; let previous_context_item = turn_context.to_turn_context_item(); let previous_turn_id = previous_context_item .turn_id .clone() .expect("turn context should have turn_id"); let current_model = "current-rollout-model"; let current_turn_id = "current-turn".to_string(); let unmatched_abort_turn_id = "other-turn".to_string(); let current_context_item = TurnContextItem { turn_id: Some(current_turn_id.clone()), #[allow(deprecated)] cwd: turn_context.cwd.to_path_buf(), workspace_roots: None, current_date: turn_context.current_date.clone(), timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), sandbox_policy: turn_context.sandbox_policy(), permission_profile: None, network: None, file_system_sandbox_policy: None, model: current_model.to_string(), comp_hash: None, personality: turn_context.personality, collaboration_mode: Some(turn_context.collaboration_mode.clone()), multi_agent_version: None, realtime_active: Some(turn_context.realtime_active), effort: turn_context.reasoning_effort.clone(), summary: codex_protocol::config_types::ReasoningSummary::Auto, }; let rollout_items = vec![ RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: previous_turn_id.clone(), trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "seed".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::TurnContext(previous_context_item), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: previous_turn_id, last_agent_message: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: current_turn_id.clone(), trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "current".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::EventMsg(EventMsg::TurnAborted( codex_protocol::protocol::TurnAbortedEvent { turn_id: Some(unmatched_abort_turn_id), reason: TurnAbortReason::Interrupted, completed_at: None, duration_ms: None, }, )), RolloutItem::TurnContext(current_context_item.clone()), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: current_turn_id, last_agent_message: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, }, )), ]; session .record_initial_history(InitialHistory::Resumed(ResumedHistory { conversation_id: ThreadId::default(), history: rollout_items, rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")), })) .await; assert_eq!( session.previous_turn_settings().await, Some(PreviousTurnSettings { model: current_model.to_string(), comp_hash: None, realtime_active: Some(turn_context.realtime_active), }) ); assert_eq!( serde_json::to_value(session.reference_context_item().await) .expect("serialize seeded reference context item"), serde_json::to_value(Some(current_context_item)) .expect("serialize expected reference context item") );}#[tokio::test]async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clears_reference_context_item() { let (session, turn_context) = make_session_and_context().await; let previous_model = "previous-rollout-model"; let previous_context_item = TurnContextItem { turn_id: Some(turn_context.sub_id.clone()), #[allow(deprecated)] cwd: turn_context.cwd.to_path_buf(), workspace_roots: None, current_date: turn_context.current_date.clone(), timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), sandbox_policy: turn_context.sandbox_policy(), permission_profile: None, network: None, file_system_sandbox_policy: None, model: previous_model.to_string(), comp_hash: None, personality: turn_context.personality, collaboration_mode: Some(turn_context.collaboration_mode.clone()), multi_agent_version: None, realtime_active: Some(turn_context.realtime_active), effort: turn_context.reasoning_effort.clone(), summary: codex_protocol::config_types::ReasoningSummary::Auto, }; let previous_turn_id = previous_context_item .turn_id .clone() .expect("turn context should have turn_id"); let incomplete_turn_id = "trailing-incomplete-turn".to_string(); let rollout_items = vec![ RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: previous_turn_id.clone(), trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "seed".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::TurnContext(previous_context_item), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: previous_turn_id, last_agent_message: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: incomplete_turn_id, trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "incomplete".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::Compacted(CompactedItem { message: String::new(), replacement_history: Some(Vec::new()), window_id: None, }), ]; session .record_initial_history(InitialHistory::Resumed(ResumedHistory { conversation_id: ThreadId::default(), history: rollout_items, rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")), })) .await; assert_eq!( session.previous_turn_settings().await, Some(PreviousTurnSettings { model: previous_model.to_string(), comp_hash: None, realtime_active: Some(turn_context.realtime_active), }) ); assert!(session.reference_context_item().await.is_none());}#[tokio::test]async fn record_initial_history_resumed_trailing_incomplete_turn_preserves_turn_context_item() { let (session, turn_context) = make_session_and_context().await; let current_context_item = turn_context.to_turn_context_item(); let current_turn_id = current_context_item .turn_id .clone() .expect("turn context should have turn_id"); let rollout_items = vec![ RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: current_turn_id, trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "incomplete".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::TurnContext(current_context_item.clone()), ]; session .record_initial_history(InitialHistory::Resumed(ResumedHistory { conversation_id: ThreadId::default(), history: rollout_items, rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")), })) .await; assert_eq!( session.previous_turn_settings().await, Some(PreviousTurnSettings { model: turn_context.model_info.slug.clone(), comp_hash: None, realtime_active: Some(turn_context.realtime_active), }) ); assert_eq!( serde_json::to_value(session.reference_context_item().await) .expect("serialize seeded reference context item"), serde_json::to_value(Some(current_context_item)) .expect("serialize expected reference context item") );}#[tokio::test]async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clears_reference_context_item() { let (session, turn_context) = make_session_and_context().await; let previous_model = "previous-rollout-model"; let previous_context_item = TurnContextItem { turn_id: Some(turn_context.sub_id.clone()), #[allow(deprecated)] cwd: turn_context.cwd.to_path_buf(), workspace_roots: None, current_date: turn_context.current_date.clone(), timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), sandbox_policy: turn_context.sandbox_policy(), permission_profile: None, network: None, file_system_sandbox_policy: None, model: previous_model.to_string(), comp_hash: None, personality: turn_context.personality, collaboration_mode: Some(turn_context.collaboration_mode.clone()), multi_agent_version: None, realtime_active: Some(turn_context.realtime_active), effort: turn_context.reasoning_effort.clone(), summary: codex_protocol::config_types::ReasoningSummary::Auto, }; let previous_turn_id = previous_context_item .turn_id .clone() .expect("turn context should have turn_id"); let compacted_incomplete_turn_id = "compacted-incomplete-turn".to_string(); let replacing_turn_id = "replacing-turn".to_string(); let rollout_items = vec![ RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: previous_turn_id.clone(), trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "seed".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::TurnContext(previous_context_item), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id: previous_turn_id, last_agent_message: None, completed_at: None, duration_ms: None, time_to_first_token_ms: None, }, )), RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: compacted_incomplete_turn_id, trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { client_id: None, message: "compacted".to_string(), images: None, local_images: Vec::new(), text_elements: Vec::new(), ..Default::default() }, )), RolloutItem::Compacted(CompactedItem { message: String::new(), replacement_history: Some(Vec::new()), window_id: None, }), // A newer TurnStarted replaces the incomplete compacted turn without a matching // completion/abort for the old one. RolloutItem::EventMsg(EventMsg::TurnStarted( codex_protocol::protocol::TurnStartedEvent { turn_id: replacing_turn_id, trace_id: None, started_at: None, model_context_window: Some(128_000), collaboration_mode_kind: ModeKind::Default, }, )), ]; session .record_initial_history(InitialHistory::Resumed(ResumedHistory { conversation_id: ThreadId::default(), history: rollout_items, rollout_path: Some(PathBuf::from("/tmp/resume.jsonl")), })) .await; assert_eq!( session.previous_turn_settings().await, Some(PreviousTurnSettings { model: previous_model.to_string(), comp_hash: None, realtime_active: Some(turn_context.realtime_active), }) ); assert!(session.reference_context_item().await.is_none());}