From f065885e2c82c85a3f62148571ca52a0a26e3f8f Mon Sep 17 00:00:00 2001 From: won Date: Thu, 19 Mar 2026 13:14:47 -0700 Subject: [PATCH 01/11] Restore image generation items in resumed thread history --- .../src/protocol/thread_history.rs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index d7482b10c31..d5e3cd523f5 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -191,6 +191,22 @@ impl ThreadHistoryBuilder { } fn handle_response_item(&mut self, item: &codex_protocol::models::ResponseItem) { + if let codex_protocol::models::ResponseItem::ImageGenerationCall { + id, + status, + revised_prompt, + result, + } = item + { + self.upsert_item_in_current_turn(ThreadItem::ImageGeneration { + id: id.clone(), + status: status.clone(), + revised_prompt: revised_prompt.clone(), + result: result.clone(), + }); + return; + } + let codex_protocol::models::ResponseItem::Message { role, content, id, .. } = item @@ -1182,6 +1198,7 @@ mod tests { use codex_protocol::items::UserMessageItem as CoreUserMessageItem; use codex_protocol::items::build_hook_prompt_message; use codex_protocol::models::MessagePhase as CoreMessagePhase; + use codex_protocol::models::ResponseItem; use codex_protocol::models::WebSearchAction as CoreWebSearchAction; use codex_protocol::parse_command::ParsedCommand; use codex_protocol::protocol::AgentMessageEvent; @@ -1385,6 +1402,65 @@ mod tests { ); } + #[test] + fn replays_image_generation_response_items_into_turn_history() { + let items = vec![ + RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { + turn_id: "turn-image".into(), + model_context_window: None, + collaboration_mode_kind: Default::default(), + })), + RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { + message: "generate an image".into(), + images: None, + text_elements: Vec::new(), + local_images: Vec::new(), + })), + RolloutItem::ResponseItem(ResponseItem::ImageGenerationCall { + id: "ig_123".into(), + status: "generating".into(), + revised_prompt: Some("draft prompt".into()), + result: String::new(), + }), + RolloutItem::ResponseItem(ResponseItem::ImageGenerationCall { + id: "ig_123".into(), + status: "completed".into(), + revised_prompt: Some("final prompt".into()), + result: "Zm9v".into(), + }), + RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { + turn_id: "turn-image".into(), + last_agent_message: None, + })), + ]; + + let turns = build_turns_from_rollout_items(&items); + assert_eq!(turns.len(), 1); + assert_eq!( + turns[0], + Turn { + id: "turn-image".into(), + status: TurnStatus::Completed, + error: None, + items: vec![ + ThreadItem::UserMessage { + id: "item-1".into(), + content: vec![UserInput::Text { + text: "generate an image".into(), + text_elements: Vec::new(), + }], + }, + ThreadItem::ImageGeneration { + id: "ig_123".into(), + status: "completed".into(), + revised_prompt: Some("final prompt".into()), + result: "Zm9v".into(), + }, + ], + } + ); + } + #[test] fn splits_reasoning_when_interleaved() { let events = vec![ From 005e6fc0cfe750bcf8c6d98130fcb5e854aefd4f Mon Sep 17 00:00:00 2001 From: won Date: Thu, 19 Mar 2026 14:15:54 -0700 Subject: [PATCH 02/11] Persist image generation end events for history replay --- .../src/protocol/thread_history.rs | 32 +++---------------- codex-rs/core/src/rollout/policy.rs | 26 ++++++++++++++- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index d5e3cd523f5..f449d5c18fd 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -191,22 +191,6 @@ impl ThreadHistoryBuilder { } fn handle_response_item(&mut self, item: &codex_protocol::models::ResponseItem) { - if let codex_protocol::models::ResponseItem::ImageGenerationCall { - id, - status, - revised_prompt, - result, - } = item - { - self.upsert_item_in_current_turn(ThreadItem::ImageGeneration { - id: id.clone(), - status: status.clone(), - revised_prompt: revised_prompt.clone(), - result: result.clone(), - }); - return; - } - let codex_protocol::models::ResponseItem::Message { role, content, id, .. } = item @@ -1198,7 +1182,6 @@ mod tests { use codex_protocol::items::UserMessageItem as CoreUserMessageItem; use codex_protocol::items::build_hook_prompt_message; use codex_protocol::models::MessagePhase as CoreMessagePhase; - use codex_protocol::models::ResponseItem; use codex_protocol::models::WebSearchAction as CoreWebSearchAction; use codex_protocol::parse_command::ParsedCommand; use codex_protocol::protocol::AgentMessageEvent; @@ -1403,7 +1386,7 @@ mod tests { } #[test] - fn replays_image_generation_response_items_into_turn_history() { + fn replays_image_generation_end_events_into_turn_history() { let items = vec![ RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { turn_id: "turn-image".into(), @@ -1416,18 +1399,13 @@ mod tests { text_elements: Vec::new(), local_images: Vec::new(), })), - RolloutItem::ResponseItem(ResponseItem::ImageGenerationCall { - id: "ig_123".into(), - status: "generating".into(), - revised_prompt: Some("draft prompt".into()), - result: String::new(), - }), - RolloutItem::ResponseItem(ResponseItem::ImageGenerationCall { - id: "ig_123".into(), + RolloutItem::EventMsg(EventMsg::ImageGenerationEnd(ImageGenerationEndEvent { + call_id: "ig_123".into(), status: "completed".into(), revised_prompt: Some("final prompt".into()), result: "Zm9v".into(), - }), + saved_path: None, + })), RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-image".into(), last_agent_message: None, diff --git a/codex-rs/core/src/rollout/policy.rs b/codex-rs/core/src/rollout/policy.rs index 4600431c644..833a82e3ba0 100644 --- a/codex-rs/core/src/rollout/policy.rs +++ b/codex-rs/core/src/rollout/policy.rs @@ -118,12 +118,12 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option { } EventMsg::Error(_) | EventMsg::GuardianAssessment(_) + | EventMsg::ImageGenerationEnd(_) | EventMsg::WebSearchEnd(_) | EventMsg::ExecCommandEnd(_) | EventMsg::PatchApplyEnd(_) | EventMsg::McpToolCallEnd(_) | EventMsg::ViewImageToolCall(_) - | EventMsg::ImageGenerationEnd(_) | EventMsg::CollabAgentSpawnEnd(_) | EventMsg::CollabAgentInteractionEnd(_) | EventMsg::CollabWaitingEnd(_) @@ -183,3 +183,27 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option { | EventMsg::ImageGenerationBegin(_) => None, } } + +#[cfg(test)] +mod tests { + use super::EventPersistenceMode; + use super::should_persist_event_msg; + use codex_protocol::protocol::EventMsg; + use codex_protocol::protocol::ImageGenerationEndEvent; + + #[test] + fn persists_image_generation_end_events_in_limited_mode() { + let event = EventMsg::ImageGenerationEnd(ImageGenerationEndEvent { + call_id: "ig_123".into(), + status: "completed".into(), + revised_prompt: Some("final prompt".into()), + result: "Zm9v".into(), + saved_path: None, + }); + + assert!(should_persist_event_msg( + &event, + EventPersistenceMode::Limited + )); + } +} From e870aae4dbb47ee7e65daa46031bebc272d9b865 Mon Sep 17 00:00:00 2001 From: won Date: Thu, 19 Mar 2026 15:17:29 -0700 Subject: [PATCH 03/11] Restore saved image paths in resumed history --- .../src/protocol/thread_history.rs | 5 +- .../app-server-protocol/src/protocol/v2.rs | 4 + .../app-server/src/codex_message_processor.rs | 93 +++++++++++++++++++ codex-rs/core/src/rollout/policy.rs | 4 +- 4 files changed, 103 insertions(+), 3 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index f449d5c18fd..11dfe297695 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -569,6 +569,7 @@ impl ThreadHistoryBuilder { status: String::new(), revised_prompt: None, result: String::new(), + saved_path: None, }; self.upsert_item_in_current_turn(item); } @@ -579,6 +580,7 @@ impl ThreadHistoryBuilder { status: payload.status.clone(), revised_prompt: payload.revised_prompt.clone(), result: payload.result.clone(), + saved_path: payload.saved_path.clone(), }; self.upsert_item_in_current_turn(item); } @@ -1404,7 +1406,7 @@ mod tests { status: "completed".into(), revised_prompt: Some("final prompt".into()), result: "Zm9v".into(), - saved_path: None, + saved_path: Some("/tmp/ig_123.png".into()), })), RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { turn_id: "turn-image".into(), @@ -1433,6 +1435,7 @@ mod tests { status: "completed".into(), revised_prompt: Some("final prompt".into()), result: "Zm9v".into(), + saved_path: Some("/tmp/ig_123.png".into()), }, ], } diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 1c8903a1449..d43581aaf70 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -4256,6 +4256,9 @@ pub enum ThreadItem { status: String, revised_prompt: Option, result: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + saved_path: Option, }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] @@ -4432,6 +4435,7 @@ impl From for ThreadItem { status: image.status, revised_prompt: image.revised_prompt, result: image.result, + saved_path: image.saved_path, }, CoreTurnItem::ContextCompaction(compaction) => { ThreadItem::ContextCompaction { id: compaction.id } diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 32882778512..e7105ceb2e2 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -7396,10 +7396,32 @@ async fn populate_thread_turns( if let Some(active_turn) = active_turn { merge_turn_history_with_active_turn(&mut turns, active_turn.clone()); } + clear_inline_image_results_for_saved_files(&mut turns); thread.turns = turns; Ok(()) } +fn clear_inline_image_results_for_saved_files(turns: &mut [Turn]) { + for turn in turns { + for item in &mut turn.items { + let ThreadItem::ImageGeneration { + result, saved_path, .. + } = item + else { + continue; + }; + + let Some(saved_path) = saved_path.as_deref() else { + continue; + }; + + if Path::new(saved_path).is_file() { + result.clear(); + } + } + } +} + async fn resolve_pending_server_request( conversation_id: ThreadId, thread_state_manager: &ThreadStateManager, @@ -9010,4 +9032,75 @@ mod tests { assert!(!manager.has_subscribers(thread_id).await); Ok(()) } + + #[tokio::test] + async fn populate_thread_turns_prefers_saved_image_path_over_inline_result() -> Result<()> { + use codex_protocol::protocol::ImageGenerationEndEvent; + use codex_protocol::protocol::RolloutItem; + use codex_protocol::protocol::TurnCompleteEvent; + use codex_protocol::protocol::TurnStartedEvent; + use codex_protocol::protocol::UserMessageEvent; + + let temp_dir = TempDir::new()?; + let saved_path = temp_dir.path().join("ig_123.png"); + std::fs::write(&saved_path, b"png-bytes")?; + let saved_path_string = saved_path.to_string_lossy().into_owned(); + + let items = vec![ + RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { + turn_id: "turn-image".into(), + model_context_window: None, + collaboration_mode_kind: Default::default(), + })), + RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { + message: "generate an image".into(), + images: None, + text_elements: Vec::new(), + local_images: Vec::new(), + })), + RolloutItem::EventMsg(EventMsg::ImageGenerationEnd(ImageGenerationEndEvent { + call_id: "ig_123".into(), + status: "completed".into(), + revised_prompt: Some("final prompt".into()), + result: "Zm9v".into(), + saved_path: Some(saved_path_string.clone()), + })), + RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { + turn_id: "turn-image".into(), + last_agent_message: None, + })), + ]; + + let mut thread = Thread { + id: "thread-image".into(), + preview: String::new(), + ephemeral: false, + model_provider: "openai".into(), + created_at: 0, + updated_at: 0, + status: ThreadStatus::NotLoaded, + path: None, + cwd: PathBuf::new(), + cli_version: String::new(), + source: SessionSource::VSCode, + agent_nickname: None, + agent_role: None, + git_info: None, + name: None, + turns: Vec::new(), + }; + + populate_thread_turns(&mut thread, ThreadTurnSource::HistoryItems(&items), None).await?; + + let Some(ThreadItem::ImageGeneration { + result, saved_path, .. + }) = thread.turns[0].items.get(1) + else { + panic!("expected image generation item"); + }; + + assert!(result.is_empty()); + assert_eq!(saved_path.as_deref(), Some(saved_path_string.as_str())); + Ok(()) + } } diff --git a/codex-rs/core/src/rollout/policy.rs b/codex-rs/core/src/rollout/policy.rs index 833a82e3ba0..8b1f94dbd56 100644 --- a/codex-rs/core/src/rollout/policy.rs +++ b/codex-rs/core/src/rollout/policy.rs @@ -105,7 +105,8 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option { | EventMsg::UndoCompleted(_) | EventMsg::TurnAborted(_) | EventMsg::TurnStarted(_) - | EventMsg::TurnComplete(_) => Some(EventPersistenceMode::Limited), + | EventMsg::TurnComplete(_) + | EventMsg::ImageGenerationEnd(_) => Some(EventPersistenceMode::Limited), EventMsg::ItemCompleted(event) => { // Plan items are derived from streaming tags and are not part of the // raw ResponseItem history, so we persist their completion to replay @@ -118,7 +119,6 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option { } EventMsg::Error(_) | EventMsg::GuardianAssessment(_) - | EventMsg::ImageGenerationEnd(_) | EventMsg::WebSearchEnd(_) | EventMsg::ExecCommandEnd(_) | EventMsg::PatchApplyEnd(_) From 85464fb6329466858d26c7646e5362121baa6287 Mon Sep 17 00:00:00 2001 From: won Date: Thu, 19 Mar 2026 19:20:50 -0700 Subject: [PATCH 04/11] Add copy guidance for generated images --- codex-rs/core/src/codex_tests.rs | 7 ++++++- codex-rs/core/src/stream_events_utils.rs | 10 ++++++---- codex-rs/tui_app_server/src/chatwidget.rs | 3 ++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index a814eab957a..a5412eff29f 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -3751,7 +3751,12 @@ async fn handle_output_item_done_records_image_save_history_message() { image_output_path.display(), )) .into(); - assert_eq!(history.raw_items(), &[save_message, item]); + let copy_message: ResponseItem = DeveloperInstructions::new( + "If you need to use a generated image at another path, copy it and leave the original in place unless the user explicitly asks you to delete it." + .to_string(), + ) + .into(); + assert_eq!(history.raw_items(), &[save_message, copy_message, item]); assert_eq!( std::fs::read(&expected_saved_path).expect("saved file"), b"foo" diff --git a/codex-rs/core/src/stream_events_utils.rs b/codex-rs/core/src/stream_events_utils.rs index 01b74f3a7e9..cd77f1d5a38 100644 --- a/codex-rs/core/src/stream_events_utils.rs +++ b/codex-rs/core/src/stream_events_utils.rs @@ -372,11 +372,13 @@ pub(crate) async fn handle_non_tool_response_item( image_output_path.display(), )) .into(); - sess.record_conversation_items( - turn_context, - std::slice::from_ref(&message), + let copy_message: ResponseItem = DeveloperInstructions::new( + "If you need to use a generated image at another path, copy it and leave the original in place unless the user explicitly asks you to delete it." + .to_string(), ) - .await; + .into(); + sess.record_conversation_items(turn_context, &[message, copy_message]) + .await; } Err(err) => { let output_path = image_generation_artifact_path( diff --git a/codex-rs/tui_app_server/src/chatwidget.rs b/codex-rs/tui_app_server/src/chatwidget.rs index 4faa8b40e71..5e0cff03c75 100644 --- a/codex-rs/tui_app_server/src/chatwidget.rs +++ b/codex-rs/tui_app_server/src/chatwidget.rs @@ -5725,13 +5725,14 @@ impl ChatWidget { status, revised_prompt, result, + saved_path, } => { self.on_image_generation_end(ImageGenerationEndEvent { call_id: id, result, revised_prompt, status, - saved_path: None, + saved_path, }); } ThreadItem::EnteredReviewMode { review, .. } => { From f1a6367e2d0514887f96b001b6a236c178654bbd Mon Sep 17 00:00:00 2001 From: won Date: Thu, 19 Mar 2026 20:01:30 -0700 Subject: [PATCH 05/11] Fix tui app server image generation adapter --- codex-rs/tui_app_server/src/app/app_server_adapter.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/codex-rs/tui_app_server/src/app/app_server_adapter.rs b/codex-rs/tui_app_server/src/app/app_server_adapter.rs index d9cd97a4feb..0d211285380 100644 --- a/codex-rs/tui_app_server/src/app/app_server_adapter.rs +++ b/codex-rs/tui_app_server/src/app/app_server_adapter.rs @@ -995,12 +995,13 @@ fn thread_item_to_core(item: &ThreadItem) -> Option { status, revised_prompt, result, + saved_path, } => Some(TurnItem::ImageGeneration(ImageGenerationItem { id: id.clone(), status: status.clone(), revised_prompt: revised_prompt.clone(), result: result.clone(), - saved_path: None, + saved_path: saved_path.clone(), })), ThreadItem::ContextCompaction { id } => { Some(TurnItem::ContextCompaction(ContextCompactionItem { @@ -1850,6 +1851,7 @@ mod tests { status: "completed".to_string(), revised_prompt: Some("diagram".to_string()), result: "image.png".to_string(), + saved_path: None, }, ThreadItem::ContextCompaction { id: "compact-1".to_string(), From ace552c658ff7c118523c4a37c72a4d088868968 Mon Sep 17 00:00:00 2001 From: won Date: Thu, 19 Mar 2026 20:23:20 -0700 Subject: [PATCH 06/11] Add tests for resumed image history preservation --- .../app-server/src/codex_message_processor.rs | 15 +++++++++---- .../src/codex/rollout_reconstruction_tests.rs | 22 +++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index e7105ceb2e2..09a0657a722 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -8731,7 +8731,7 @@ mod tests { model_provider: "test-provider".to_string(), cwd: PathBuf::from("/"), cli_version: "0.0.0".to_string(), - source: SessionSource::VSCode, + source: SessionSource::VSCode.into(), git_info: None, }; @@ -8787,7 +8787,7 @@ mod tests { model_provider: "fallback".to_string(), cwd: PathBuf::new(), cli_version: String::new(), - source: SessionSource::VSCode, + source: codex_app_server_protocol::SessionSource::VsCode.into(), git_info: None, }; @@ -9082,7 +9082,7 @@ mod tests { path: None, cwd: PathBuf::new(), cli_version: String::new(), - source: SessionSource::VSCode, + source: SessionSource::VSCode.into(), agent_nickname: None, agent_role: None, git_info: None, @@ -9090,7 +9090,9 @@ mod tests { turns: Vec::new(), }; - populate_thread_turns(&mut thread, ThreadTurnSource::HistoryItems(&items), None).await?; + populate_thread_turns(&mut thread, ThreadTurnSource::HistoryItems(&items), None) + .await + .map_err(anyhow::Error::msg)?; let Some(ThreadItem::ImageGeneration { result, saved_path, .. @@ -9101,6 +9103,11 @@ mod tests { assert!(result.is_empty()); assert_eq!(saved_path.as_deref(), Some(saved_path_string.as_str())); + let Some(RolloutItem::EventMsg(EventMsg::ImageGenerationEnd(source_item))) = items.get(2) else { + panic!("expected source image generation end event"); + }; + assert_eq!(source_item.result, "Zm9v"); + assert_eq!(source_item.saved_path.as_deref(), Some(saved_path_string.as_str())); Ok(()) } } diff --git a/codex-rs/core/src/codex/rollout_reconstruction_tests.rs b/codex-rs/core/src/codex/rollout_reconstruction_tests.rs index 6cc99a29074..e1eded25df0 100644 --- a/codex-rs/core/src/codex/rollout_reconstruction_tests.rs +++ b/codex-rs/core/src/codex/rollout_reconstruction_tests.rs @@ -33,6 +33,28 @@ fn assistant_message(text: &str) -> ResponseItem { } } +#[tokio::test] +async fn record_initial_history_resumed_preserves_model_visible_image_generation_result() { + let (session, _turn_context) = make_session_and_context().await; + let item = ResponseItem::ImageGenerationCall { + id: "ig-test".to_string(), + status: "completed".to_string(), + revised_prompt: Some("a tiny blue square".to_string()), + result: "Zm9v".to_string(), + }; + + session + .record_initial_history(InitialHistory::Resumed(ResumedHistory { + conversation_id: ThreadId::default(), + history: vec![RolloutItem::ResponseItem(item.clone())], + rollout_path: PathBuf::from("/tmp/resume.jsonl"), + })) + .await; + + let history = session.clone_history().await; + assert_eq!(history.raw_items(), &[item]); +} + #[tokio::test] async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previous_turn_settings() { From b1ed4a7cb50fedd801033ba1532586548249606b Mon Sep 17 00:00:00 2001 From: won Date: Thu, 19 Mar 2026 20:29:04 -0700 Subject: [PATCH 07/11] Remove temporary image history proof tests --- .../app-server/src/codex_message_processor.rs | 5 ----- .../src/codex/rollout_reconstruction_tests.rs | 22 ------------------- 2 files changed, 27 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 09a0657a722..dac87d7fe39 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -9103,11 +9103,6 @@ mod tests { assert!(result.is_empty()); assert_eq!(saved_path.as_deref(), Some(saved_path_string.as_str())); - let Some(RolloutItem::EventMsg(EventMsg::ImageGenerationEnd(source_item))) = items.get(2) else { - panic!("expected source image generation end event"); - }; - assert_eq!(source_item.result, "Zm9v"); - assert_eq!(source_item.saved_path.as_deref(), Some(saved_path_string.as_str())); Ok(()) } } diff --git a/codex-rs/core/src/codex/rollout_reconstruction_tests.rs b/codex-rs/core/src/codex/rollout_reconstruction_tests.rs index e1eded25df0..6cc99a29074 100644 --- a/codex-rs/core/src/codex/rollout_reconstruction_tests.rs +++ b/codex-rs/core/src/codex/rollout_reconstruction_tests.rs @@ -33,28 +33,6 @@ fn assistant_message(text: &str) -> ResponseItem { } } -#[tokio::test] -async fn record_initial_history_resumed_preserves_model_visible_image_generation_result() { - let (session, _turn_context) = make_session_and_context().await; - let item = ResponseItem::ImageGenerationCall { - id: "ig-test".to_string(), - status: "completed".to_string(), - revised_prompt: Some("a tiny blue square".to_string()), - result: "Zm9v".to_string(), - }; - - session - .record_initial_history(InitialHistory::Resumed(ResumedHistory { - conversation_id: ThreadId::default(), - history: vec![RolloutItem::ResponseItem(item.clone())], - rollout_path: PathBuf::from("/tmp/resume.jsonl"), - })) - .await; - - let history = session.clone_history().await; - assert_eq!(history.raw_items(), &[item]); -} - #[tokio::test] async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previous_turn_settings() { From 050a4783b9da451f169a9fafb76e05efa90daa34 Mon Sep 17 00:00:00 2001 From: won Date: Thu, 19 Mar 2026 20:41:33 -0700 Subject: [PATCH 08/11] Avoid duplicating saved image blobs in limited rollouts --- codex-rs/core/src/rollout/recorder.rs | 15 ++++-- codex-rs/core/src/rollout/recorder_tests.rs | 57 +++++++++++++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/codex-rs/core/src/rollout/recorder.rs b/codex-rs/core/src/rollout/recorder.rs index 002269d59ee..83320c08739 100644 --- a/codex-rs/core/src/rollout/recorder.rs +++ b/codex-rs/core/src/rollout/recorder.rs @@ -138,12 +138,19 @@ fn sanitize_rollout_item_for_persistence( item: RolloutItem, mode: EventPersistenceMode, ) -> RolloutItem { - if mode != EventPersistenceMode::Extended { - return item; - } - match item { + RolloutItem::EventMsg(EventMsg::ImageGenerationEnd(mut event)) + if mode == EventPersistenceMode::Limited && event.saved_path.is_some() => + { + // ResponseItem::ImageGenerationCall already persists the inline base64 payload. + // Keep only the saved-path metadata in limited rollouts to avoid storing the blob twice. + event.result.clear(); + RolloutItem::EventMsg(EventMsg::ImageGenerationEnd(event)) + } RolloutItem::EventMsg(EventMsg::ExecCommandEnd(mut event)) => { + if mode != EventPersistenceMode::Extended { + return RolloutItem::EventMsg(EventMsg::ExecCommandEnd(event)); + } // Persist only a bounded aggregated summary of command output. event.aggregated_output = truncate_text( &event.aggregated_output, diff --git a/codex-rs/core/src/rollout/recorder_tests.rs b/codex-rs/core/src/rollout/recorder_tests.rs index 8ca7b58a6b5..eb4206c31a8 100644 --- a/codex-rs/core/src/rollout/recorder_tests.rs +++ b/codex-rs/core/src/rollout/recorder_tests.rs @@ -510,3 +510,60 @@ async fn resume_candidate_matches_cwd_reads_latest_turn_context() -> std::io::Re ); Ok(()) } + +#[tokio::test] +async fn limited_rollout_clears_inline_image_result_when_saved_path_exists() +-> std::io::Result<()> { + let home = TempDir::new().expect("temp dir"); + let config = ConfigBuilder::default() + .codex_home(home.path().to_path_buf()) + .build() + .await?; + let thread_id = ThreadId::new(); + let recorder = RolloutRecorder::new( + &config, + RolloutRecorderParams::new( + thread_id, + None, + SessionSource::Cli, + BaseInstructions::default(), + Vec::new(), + EventPersistenceMode::Limited, + ), + None, + None, + ) + .await?; + + let saved_path = home.path().join("generated_images/thread/ig_123.png"); + let saved_path_str = saved_path.to_string_lossy().into_owned(); + recorder + .record_items(&[RolloutItem::EventMsg(EventMsg::ImageGenerationEnd( + codex_protocol::protocol::ImageGenerationEndEvent { + call_id: "ig_123".to_string(), + status: "completed".to_string(), + revised_prompt: Some("final prompt".to_string()), + result: "Zm9v".to_string(), + saved_path: Some(saved_path_str.clone()), + }, + ))]) + .await?; + recorder.persist().await?; + recorder.flush().await?; + + let text = std::fs::read_to_string(recorder.rollout_path())?; + let persisted_event = text + .lines() + .filter_map(|line| serde_json::from_str::(line).ok()) + .find_map(|line| match line.item { + RolloutItem::EventMsg(EventMsg::ImageGenerationEnd(event)) => Some(event), + _ => None, + }) + .expect("persisted image generation end event"); + + assert_eq!(persisted_event.saved_path.as_deref(), Some(saved_path_str.as_str())); + assert!(persisted_event.result.is_empty()); + + recorder.shutdown().await?; + Ok(()) +} From 66aa6d19363847463f3283d9462a03583b1ad2f1 Mon Sep 17 00:00:00 2001 From: won Date: Thu, 19 Mar 2026 21:00:46 -0700 Subject: [PATCH 09/11] Remove rollout image persistence test --- codex-rs/core/src/rollout/recorder_tests.rs | 57 --------------------- 1 file changed, 57 deletions(-) diff --git a/codex-rs/core/src/rollout/recorder_tests.rs b/codex-rs/core/src/rollout/recorder_tests.rs index eb4206c31a8..8ca7b58a6b5 100644 --- a/codex-rs/core/src/rollout/recorder_tests.rs +++ b/codex-rs/core/src/rollout/recorder_tests.rs @@ -510,60 +510,3 @@ async fn resume_candidate_matches_cwd_reads_latest_turn_context() -> std::io::Re ); Ok(()) } - -#[tokio::test] -async fn limited_rollout_clears_inline_image_result_when_saved_path_exists() --> std::io::Result<()> { - let home = TempDir::new().expect("temp dir"); - let config = ConfigBuilder::default() - .codex_home(home.path().to_path_buf()) - .build() - .await?; - let thread_id = ThreadId::new(); - let recorder = RolloutRecorder::new( - &config, - RolloutRecorderParams::new( - thread_id, - None, - SessionSource::Cli, - BaseInstructions::default(), - Vec::new(), - EventPersistenceMode::Limited, - ), - None, - None, - ) - .await?; - - let saved_path = home.path().join("generated_images/thread/ig_123.png"); - let saved_path_str = saved_path.to_string_lossy().into_owned(); - recorder - .record_items(&[RolloutItem::EventMsg(EventMsg::ImageGenerationEnd( - codex_protocol::protocol::ImageGenerationEndEvent { - call_id: "ig_123".to_string(), - status: "completed".to_string(), - revised_prompt: Some("final prompt".to_string()), - result: "Zm9v".to_string(), - saved_path: Some(saved_path_str.clone()), - }, - ))]) - .await?; - recorder.persist().await?; - recorder.flush().await?; - - let text = std::fs::read_to_string(recorder.rollout_path())?; - let persisted_event = text - .lines() - .filter_map(|line| serde_json::from_str::(line).ok()) - .find_map(|line| match line.item { - RolloutItem::EventMsg(EventMsg::ImageGenerationEnd(event)) => Some(event), - _ => None, - }) - .expect("persisted image generation end event"); - - assert_eq!(persisted_event.saved_path.as_deref(), Some(saved_path_str.as_str())); - assert!(persisted_event.result.is_empty()); - - recorder.shutdown().await?; - Ok(()) -} From 6e7d37205eca1e7f2f9915b9306163d4bd7dbbdd Mon Sep 17 00:00:00 2001 From: won Date: Thu, 19 Mar 2026 21:15:56 -0700 Subject: [PATCH 10/11] Revert rollout image blob clearing --- codex-rs/core/src/rollout/recorder.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/codex-rs/core/src/rollout/recorder.rs b/codex-rs/core/src/rollout/recorder.rs index 83320c08739..99e0a77eeb3 100644 --- a/codex-rs/core/src/rollout/recorder.rs +++ b/codex-rs/core/src/rollout/recorder.rs @@ -139,14 +139,6 @@ fn sanitize_rollout_item_for_persistence( mode: EventPersistenceMode, ) -> RolloutItem { match item { - RolloutItem::EventMsg(EventMsg::ImageGenerationEnd(mut event)) - if mode == EventPersistenceMode::Limited && event.saved_path.is_some() => - { - // ResponseItem::ImageGenerationCall already persists the inline base64 payload. - // Keep only the saved-path metadata in limited rollouts to avoid storing the blob twice. - event.result.clear(); - RolloutItem::EventMsg(EventMsg::ImageGenerationEnd(event)) - } RolloutItem::EventMsg(EventMsg::ExecCommandEnd(mut event)) => { if mode != EventPersistenceMode::Extended { return RolloutItem::EventMsg(EventMsg::ExecCommandEnd(event)); From 0552a98268e50d0397f6fcde32020648a530ebb1 Mon Sep 17 00:00:00 2001 From: won Date: Thu, 19 Mar 2026 21:38:25 -0700 Subject: [PATCH 11/11] Trim restore image history branch scope --- .../schema/json/ServerNotification.json | 6 ++ .../codex_app_server_protocol.schemas.json | 6 ++ .../codex_app_server_protocol.v2.schemas.json | 6 ++ .../json/v2/ItemCompletedNotification.json | 6 ++ .../json/v2/ItemStartedNotification.json | 6 ++ .../schema/json/v2/ReviewStartResponse.json | 6 ++ .../schema/json/v2/ThreadForkResponse.json | 6 ++ .../schema/json/v2/ThreadListResponse.json | 6 ++ .../json/v2/ThreadMetadataUpdateResponse.json | 6 ++ .../schema/json/v2/ThreadReadResponse.json | 6 ++ .../schema/json/v2/ThreadResumeResponse.json | 6 ++ .../json/v2/ThreadRollbackResponse.json | 6 ++ .../schema/json/v2/ThreadStartResponse.json | 6 ++ .../json/v2/ThreadStartedNotification.json | 6 ++ .../json/v2/ThreadUnarchiveResponse.json | 6 ++ .../json/v2/TurnCompletedNotification.json | 6 ++ .../schema/json/v2/TurnStartResponse.json | 6 ++ .../json/v2/TurnStartedNotification.json | 6 ++ .../schema/typescript/v2/ThreadItem.ts | 2 +- .../app-server/src/codex_message_processor.rs | 99 +------------------ codex-rs/core/src/rollout/recorder.rs | 7 +- 21 files changed, 115 insertions(+), 101 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index f9cbe76e2a7..045301e090f 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -2817,6 +2817,12 @@ "null" ] }, + "savedPath": { + "type": [ + "string", + "null" + ] + }, "status": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 68bf7477e57..3d392be1a04 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -12540,6 +12540,12 @@ "null" ] }, + "savedPath": { + "type": [ + "string", + "null" + ] + }, "status": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 772eb6f47ae..e06b5d1a167 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -10300,6 +10300,12 @@ "null" ] }, + "savedPath": { + "type": [ + "string", + "null" + ] + }, "status": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index 3b974662023..39641078658 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -1026,6 +1026,12 @@ "null" ] }, + "savedPath": { + "type": [ + "string", + "null" + ] + }, "status": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index b77b34536c3..abb8aee5dc8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -1026,6 +1026,12 @@ "null" ] }, + "savedPath": { + "type": [ + "string", + "null" + ] + }, "status": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index 7f4a2b1f447..98b485b5781 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -1140,6 +1140,12 @@ "null" ] }, + "savedPath": { + "type": [ + "string", + "null" + ] + }, "status": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 44734226d5d..8aee99f90c0 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -1633,6 +1633,12 @@ "null" ] }, + "savedPath": { + "type": [ + "string", + "null" + ] + }, "status": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index 766fe48cef6..05f3ae87c00 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -1391,6 +1391,12 @@ "null" ] }, + "savedPath": { + "type": [ + "string", + "null" + ] + }, "status": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index 1ef137f9ebf..214c25f5401 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -1391,6 +1391,12 @@ "null" ] }, + "savedPath": { + "type": [ + "string", + "null" + ] + }, "status": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index 3b7726c423e..2a8fe06ece6 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -1391,6 +1391,12 @@ "null" ] }, + "savedPath": { + "type": [ + "string", + "null" + ] + }, "status": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index ba42df4acc0..468325cef17 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -1633,6 +1633,12 @@ "null" ] }, + "savedPath": { + "type": [ + "string", + "null" + ] + }, "status": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index bb9dcbdd972..def818dcfa7 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -1391,6 +1391,12 @@ "null" ] }, + "savedPath": { + "type": [ + "string", + "null" + ] + }, "status": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index ba71383208f..c225b1c0f2f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -1633,6 +1633,12 @@ "null" ] }, + "savedPath": { + "type": [ + "string", + "null" + ] + }, "status": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index 53806b272b6..df7670cdb71 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -1391,6 +1391,12 @@ "null" ] }, + "savedPath": { + "type": [ + "string", + "null" + ] + }, "status": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index 3430d24e3e8..d95cd4dd89d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -1391,6 +1391,12 @@ "null" ] }, + "savedPath": { + "type": [ + "string", + "null" + ] + }, "status": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index 40ce73e5218..b0220247aaf 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -1140,6 +1140,12 @@ "null" ] }, + "savedPath": { + "type": [ + "string", + "null" + ] + }, "status": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index 954321c168b..cd9f63bb6ca 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -1140,6 +1140,12 @@ "null" ] }, + "savedPath": { + "type": [ + "string", + "null" + ] + }, "status": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index 66ce683739f..3cc16db9227 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -1140,6 +1140,12 @@ "null" ] }, + "savedPath": { + "type": [ + "string", + "null" + ] + }, "status": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts index f1f864ae4a6..9202f3728f0 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts @@ -97,4 +97,4 @@ reasoningEffort: ReasoningEffort | null, /** * Last known status of the target agents, when available. */ -agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: string, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, }; +agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: string, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: string, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, }; diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index dac87d7fe39..32882778512 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -7396,32 +7396,10 @@ async fn populate_thread_turns( if let Some(active_turn) = active_turn { merge_turn_history_with_active_turn(&mut turns, active_turn.clone()); } - clear_inline_image_results_for_saved_files(&mut turns); thread.turns = turns; Ok(()) } -fn clear_inline_image_results_for_saved_files(turns: &mut [Turn]) { - for turn in turns { - for item in &mut turn.items { - let ThreadItem::ImageGeneration { - result, saved_path, .. - } = item - else { - continue; - }; - - let Some(saved_path) = saved_path.as_deref() else { - continue; - }; - - if Path::new(saved_path).is_file() { - result.clear(); - } - } - } -} - async fn resolve_pending_server_request( conversation_id: ThreadId, thread_state_manager: &ThreadStateManager, @@ -8731,7 +8709,7 @@ mod tests { model_provider: "test-provider".to_string(), cwd: PathBuf::from("/"), cli_version: "0.0.0".to_string(), - source: SessionSource::VSCode.into(), + source: SessionSource::VSCode, git_info: None, }; @@ -8787,7 +8765,7 @@ mod tests { model_provider: "fallback".to_string(), cwd: PathBuf::new(), cli_version: String::new(), - source: codex_app_server_protocol::SessionSource::VsCode.into(), + source: SessionSource::VSCode, git_info: None, }; @@ -9032,77 +9010,4 @@ mod tests { assert!(!manager.has_subscribers(thread_id).await); Ok(()) } - - #[tokio::test] - async fn populate_thread_turns_prefers_saved_image_path_over_inline_result() -> Result<()> { - use codex_protocol::protocol::ImageGenerationEndEvent; - use codex_protocol::protocol::RolloutItem; - use codex_protocol::protocol::TurnCompleteEvent; - use codex_protocol::protocol::TurnStartedEvent; - use codex_protocol::protocol::UserMessageEvent; - - let temp_dir = TempDir::new()?; - let saved_path = temp_dir.path().join("ig_123.png"); - std::fs::write(&saved_path, b"png-bytes")?; - let saved_path_string = saved_path.to_string_lossy().into_owned(); - - let items = vec![ - RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent { - turn_id: "turn-image".into(), - model_context_window: None, - collaboration_mode_kind: Default::default(), - })), - RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { - message: "generate an image".into(), - images: None, - text_elements: Vec::new(), - local_images: Vec::new(), - })), - RolloutItem::EventMsg(EventMsg::ImageGenerationEnd(ImageGenerationEndEvent { - call_id: "ig_123".into(), - status: "completed".into(), - revised_prompt: Some("final prompt".into()), - result: "Zm9v".into(), - saved_path: Some(saved_path_string.clone()), - })), - RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent { - turn_id: "turn-image".into(), - last_agent_message: None, - })), - ]; - - let mut thread = Thread { - id: "thread-image".into(), - preview: String::new(), - ephemeral: false, - model_provider: "openai".into(), - created_at: 0, - updated_at: 0, - status: ThreadStatus::NotLoaded, - path: None, - cwd: PathBuf::new(), - cli_version: String::new(), - source: SessionSource::VSCode.into(), - agent_nickname: None, - agent_role: None, - git_info: None, - name: None, - turns: Vec::new(), - }; - - populate_thread_turns(&mut thread, ThreadTurnSource::HistoryItems(&items), None) - .await - .map_err(anyhow::Error::msg)?; - - let Some(ThreadItem::ImageGeneration { - result, saved_path, .. - }) = thread.turns[0].items.get(1) - else { - panic!("expected image generation item"); - }; - - assert!(result.is_empty()); - assert_eq!(saved_path.as_deref(), Some(saved_path_string.as_str())); - Ok(()) - } } diff --git a/codex-rs/core/src/rollout/recorder.rs b/codex-rs/core/src/rollout/recorder.rs index 99e0a77eeb3..002269d59ee 100644 --- a/codex-rs/core/src/rollout/recorder.rs +++ b/codex-rs/core/src/rollout/recorder.rs @@ -138,11 +138,12 @@ fn sanitize_rollout_item_for_persistence( item: RolloutItem, mode: EventPersistenceMode, ) -> RolloutItem { + if mode != EventPersistenceMode::Extended { + return item; + } + match item { RolloutItem::EventMsg(EventMsg::ExecCommandEnd(mut event)) => { - if mode != EventPersistenceMode::Extended { - return RolloutItem::EventMsg(EventMsg::ExecCommandEnd(event)); - } // Persist only a bounded aggregated summary of command output. event.aggregated_output = truncate_text( &event.aggregated_output,