@@ -130,6 +130,8 @@ use codex_protocol::models::ShellToolCallParams;
130130use codex_protocol:: protocol:: InitialHistory ;
131131
132132mod compact;
133+ use self :: compact:: build_compacted_history;
134+ use self :: compact:: collect_user_messages;
133135
134136// A convenience extension trait for acquiring mutex locks where poisoning is
135137// unrecoverable and should abort the program. This avoids scattered `.unwrap()`
@@ -205,7 +207,7 @@ impl Codex {
205207 config. clone ( ) ,
206208 auth_manager. clone ( ) ,
207209 tx_event. clone ( ) ,
208- conversation_history. clone ( ) ,
210+ conversation_history,
209211 )
210212 . await
211213 . map_err ( |e| {
@@ -564,9 +566,10 @@ impl Session {
564566 let persist = matches ! ( conversation_history, InitialHistory :: Forked ( _) ) ;
565567
566568 // Always add response items to conversation history
567- let response_items = conversation_history. get_response_items ( ) ;
568- if !response_items. is_empty ( ) {
569- self . record_into_history ( & response_items) ;
569+ let reconstructed_history =
570+ self . reconstruct_history_from_rollout ( turn_context, & rollout_items) ;
571+ if !reconstructed_history. is_empty ( ) {
572+ self . record_into_history ( & reconstructed_history) ;
570573 }
571574
572575 // If persisting, persist all rollout items as-is (recorder filters)
@@ -678,6 +681,33 @@ impl Session {
678681 self . persist_rollout_response_items ( items) . await ;
679682 }
680683
684+ fn reconstruct_history_from_rollout (
685+ & self ,
686+ turn_context : & TurnContext ,
687+ rollout_items : & [ RolloutItem ] ,
688+ ) -> Vec < ResponseItem > {
689+ let mut history = ConversationHistory :: new ( ) ;
690+ for item in rollout_items {
691+ match item {
692+ RolloutItem :: ResponseItem ( response_item) => {
693+ history. record_items ( std:: iter:: once ( response_item) ) ;
694+ }
695+ RolloutItem :: Compacted ( compacted) => {
696+ let snapshot = history. contents ( ) ;
697+ let user_messages = collect_user_messages ( & snapshot) ;
698+ let rebuilt = build_compacted_history (
699+ self . build_initial_context ( turn_context) ,
700+ & user_messages,
701+ & compacted. message ,
702+ ) ;
703+ history. replace ( rebuilt) ;
704+ }
705+ _ => { }
706+ }
707+ }
708+ history. contents ( )
709+ }
710+
681711 /// Append ResponseItems to the in-memory conversation history only.
682712 fn record_into_history ( & self , items : & [ ResponseItem ] ) {
683713 self . state
@@ -3220,18 +3250,59 @@ async fn exit_review_mode(
32203250#[ cfg( test) ]
32213251mod tests {
32223252 use super :: * ;
3253+ use crate :: config:: ConfigOverrides ;
3254+ use crate :: config:: ConfigToml ;
3255+ use crate :: protocol:: CompactedItem ;
3256+ use crate :: protocol:: InitialHistory ;
3257+ use crate :: protocol:: ResumedHistory ;
3258+ use codex_protocol:: models:: ContentItem ;
32233259 use mcp_types:: ContentBlock ;
32243260 use mcp_types:: TextContent ;
32253261 use pretty_assertions:: assert_eq;
32263262 use serde_json:: json;
3263+ use std:: path:: PathBuf ;
3264+ use std:: sync:: Arc ;
32273265 use std:: time:: Duration as StdDuration ;
32283266
3229- fn text_block ( s : & str ) -> ContentBlock {
3230- ContentBlock :: TextContent ( TextContent {
3231- annotations : None ,
3232- text : s. to_string ( ) ,
3233- r#type : "text" . to_string ( ) ,
3234- } )
3267+ #[ test]
3268+ fn reconstruct_history_matches_live_compactions ( ) {
3269+ let ( session, turn_context) = make_session_and_context ( ) ;
3270+ let ( rollout_items, expected) = sample_rollout ( & session, & turn_context) ;
3271+
3272+ let reconstructed = session. reconstruct_history_from_rollout ( & turn_context, & rollout_items) ;
3273+
3274+ assert_eq ! ( expected, reconstructed) ;
3275+ }
3276+
3277+ #[ test]
3278+ fn record_initial_history_reconstructs_resumed_transcript ( ) {
3279+ let ( session, turn_context) = make_session_and_context ( ) ;
3280+ let ( rollout_items, expected) = sample_rollout ( & session, & turn_context) ;
3281+
3282+ tokio_test:: block_on ( session. record_initial_history (
3283+ & turn_context,
3284+ InitialHistory :: Resumed ( ResumedHistory {
3285+ conversation_id : ConversationId :: default ( ) ,
3286+ history : rollout_items,
3287+ rollout_path : PathBuf :: from ( "/tmp/resume.jsonl" ) ,
3288+ } ) ,
3289+ ) ) ;
3290+
3291+ let actual = session. state . lock_unchecked ( ) . history . contents ( ) ;
3292+ assert_eq ! ( expected, actual) ;
3293+ }
3294+
3295+ #[ test]
3296+ fn record_initial_history_reconstructs_forked_transcript ( ) {
3297+ let ( session, turn_context) = make_session_and_context ( ) ;
3298+ let ( rollout_items, expected) = sample_rollout ( & session, & turn_context) ;
3299+
3300+ tokio_test:: block_on (
3301+ session. record_initial_history ( & turn_context, InitialHistory :: Forked ( rollout_items) ) ,
3302+ ) ;
3303+
3304+ let actual = session. state . lock_unchecked ( ) . history . contents ( ) ;
3305+ assert_eq ! ( expected, actual) ;
32353306 }
32363307
32373308 #[ test]
@@ -3386,4 +3457,174 @@ mod tests {
33863457
33873458 assert_eq ! ( expected, got) ;
33883459 }
3460+
3461+ fn text_block ( s : & str ) -> ContentBlock {
3462+ ContentBlock :: TextContent ( TextContent {
3463+ annotations : None ,
3464+ text : s. to_string ( ) ,
3465+ r#type : "text" . to_string ( ) ,
3466+ } )
3467+ }
3468+
3469+ fn make_session_and_context ( ) -> ( Session , TurnContext ) {
3470+ let ( tx_event, _rx_event) = async_channel:: unbounded ( ) ;
3471+ let codex_home = tempfile:: tempdir ( ) . expect ( "create temp dir" ) ;
3472+ let config = Config :: load_from_base_config_with_overrides (
3473+ ConfigToml :: default ( ) ,
3474+ ConfigOverrides :: default ( ) ,
3475+ codex_home. path ( ) . to_path_buf ( ) ,
3476+ )
3477+ . expect ( "load default test config" ) ;
3478+ let config = Arc :: new ( config) ;
3479+ let conversation_id = ConversationId :: default ( ) ;
3480+ let client = ModelClient :: new (
3481+ config. clone ( ) ,
3482+ None ,
3483+ config. model_provider . clone ( ) ,
3484+ config. model_reasoning_effort ,
3485+ config. model_reasoning_summary ,
3486+ conversation_id,
3487+ ) ;
3488+ let tools_config = ToolsConfig :: new ( & ToolsConfigParams {
3489+ model_family : & config. model_family ,
3490+ approval_policy : config. approval_policy ,
3491+ sandbox_policy : config. sandbox_policy . clone ( ) ,
3492+ include_plan_tool : config. include_plan_tool ,
3493+ include_apply_patch_tool : config. include_apply_patch_tool ,
3494+ include_web_search_request : config. tools_web_search_request ,
3495+ use_streamable_shell_tool : config. use_experimental_streamable_shell_tool ,
3496+ include_view_image_tool : config. include_view_image_tool ,
3497+ experimental_unified_exec_tool : config. use_experimental_unified_exec_tool ,
3498+ } ) ;
3499+ let turn_context = TurnContext {
3500+ client,
3501+ cwd : config. cwd . clone ( ) ,
3502+ base_instructions : config. base_instructions . clone ( ) ,
3503+ user_instructions : config. user_instructions . clone ( ) ,
3504+ approval_policy : config. approval_policy ,
3505+ sandbox_policy : config. sandbox_policy . clone ( ) ,
3506+ shell_environment_policy : config. shell_environment_policy . clone ( ) ,
3507+ tools_config,
3508+ is_review_mode : false ,
3509+ } ;
3510+ let session = Session {
3511+ conversation_id,
3512+ tx_event,
3513+ mcp_connection_manager : McpConnectionManager :: default ( ) ,
3514+ session_manager : ExecSessionManager :: default ( ) ,
3515+ unified_exec_manager : UnifiedExecSessionManager :: default ( ) ,
3516+ notify : None ,
3517+ rollout : Mutex :: new ( None ) ,
3518+ state : Mutex :: new ( State {
3519+ history : ConversationHistory :: new ( ) ,
3520+ ..Default :: default ( )
3521+ } ) ,
3522+ codex_linux_sandbox_exe : None ,
3523+ user_shell : shell:: Shell :: Unknown ,
3524+ show_raw_agent_reasoning : config. show_raw_agent_reasoning ,
3525+ } ;
3526+ ( session, turn_context)
3527+ }
3528+
3529+ fn sample_rollout (
3530+ session : & Session ,
3531+ turn_context : & TurnContext ,
3532+ ) -> ( Vec < RolloutItem > , Vec < ResponseItem > ) {
3533+ let mut rollout_items = Vec :: new ( ) ;
3534+ let mut live_history = ConversationHistory :: new ( ) ;
3535+
3536+ let initial_context = session. build_initial_context ( turn_context) ;
3537+ for item in & initial_context {
3538+ rollout_items. push ( RolloutItem :: ResponseItem ( item. clone ( ) ) ) ;
3539+ }
3540+ live_history. record_items ( initial_context. iter ( ) ) ;
3541+
3542+ let user1 = ResponseItem :: Message {
3543+ id : None ,
3544+ role : "user" . to_string ( ) ,
3545+ content : vec ! [ ContentItem :: InputText {
3546+ text: "first user" . to_string( ) ,
3547+ } ] ,
3548+ } ;
3549+ live_history. record_items ( std:: iter:: once ( & user1) ) ;
3550+ rollout_items. push ( RolloutItem :: ResponseItem ( user1. clone ( ) ) ) ;
3551+
3552+ let assistant1 = ResponseItem :: Message {
3553+ id : None ,
3554+ role : "assistant" . to_string ( ) ,
3555+ content : vec ! [ ContentItem :: OutputText {
3556+ text: "assistant reply one" . to_string( ) ,
3557+ } ] ,
3558+ } ;
3559+ live_history. record_items ( std:: iter:: once ( & assistant1) ) ;
3560+ rollout_items. push ( RolloutItem :: ResponseItem ( assistant1. clone ( ) ) ) ;
3561+
3562+ let summary1 = "summary one" ;
3563+ let snapshot1 = live_history. contents ( ) ;
3564+ let user_messages1 = collect_user_messages ( & snapshot1) ;
3565+ let rebuilt1 = build_compacted_history (
3566+ session. build_initial_context ( turn_context) ,
3567+ & user_messages1,
3568+ summary1,
3569+ ) ;
3570+ live_history. replace ( rebuilt1) ;
3571+ rollout_items. push ( RolloutItem :: Compacted ( CompactedItem {
3572+ message : summary1. to_string ( ) ,
3573+ } ) ) ;
3574+
3575+ let user2 = ResponseItem :: Message {
3576+ id : None ,
3577+ role : "user" . to_string ( ) ,
3578+ content : vec ! [ ContentItem :: InputText {
3579+ text: "second user" . to_string( ) ,
3580+ } ] ,
3581+ } ;
3582+ live_history. record_items ( std:: iter:: once ( & user2) ) ;
3583+ rollout_items. push ( RolloutItem :: ResponseItem ( user2. clone ( ) ) ) ;
3584+
3585+ let assistant2 = ResponseItem :: Message {
3586+ id : None ,
3587+ role : "assistant" . to_string ( ) ,
3588+ content : vec ! [ ContentItem :: OutputText {
3589+ text: "assistant reply two" . to_string( ) ,
3590+ } ] ,
3591+ } ;
3592+ live_history. record_items ( std:: iter:: once ( & assistant2) ) ;
3593+ rollout_items. push ( RolloutItem :: ResponseItem ( assistant2. clone ( ) ) ) ;
3594+
3595+ let summary2 = "summary two" ;
3596+ let snapshot2 = live_history. contents ( ) ;
3597+ let user_messages2 = collect_user_messages ( & snapshot2) ;
3598+ let rebuilt2 = build_compacted_history (
3599+ session. build_initial_context ( turn_context) ,
3600+ & user_messages2,
3601+ summary2,
3602+ ) ;
3603+ live_history. replace ( rebuilt2) ;
3604+ rollout_items. push ( RolloutItem :: Compacted ( CompactedItem {
3605+ message : summary2. to_string ( ) ,
3606+ } ) ) ;
3607+
3608+ let user3 = ResponseItem :: Message {
3609+ id : None ,
3610+ role : "user" . to_string ( ) ,
3611+ content : vec ! [ ContentItem :: InputText {
3612+ text: "third user" . to_string( ) ,
3613+ } ] ,
3614+ } ;
3615+ live_history. record_items ( std:: iter:: once ( & user3) ) ;
3616+ rollout_items. push ( RolloutItem :: ResponseItem ( user3. clone ( ) ) ) ;
3617+
3618+ let assistant3 = ResponseItem :: Message {
3619+ id : None ,
3620+ role : "assistant" . to_string ( ) ,
3621+ content : vec ! [ ContentItem :: OutputText {
3622+ text: "assistant reply three" . to_string( ) ,
3623+ } ] ,
3624+ } ;
3625+ live_history. record_items ( std:: iter:: once ( & assistant3) ) ;
3626+ rollout_items. push ( RolloutItem :: ResponseItem ( assistant3. clone ( ) ) ) ;
3627+
3628+ ( rollout_items, live_history. contents ( ) )
3629+ }
33893630}
0 commit comments