From 7eaa398f009339392f4223e4a611e6bd475150d8 Mon Sep 17 00:00:00 2001 From: won Date: Tue, 16 Jun 2026 21:16:53 -0700 Subject: [PATCH 1/3] Persist terminal image generation results --- codex-rs/core/src/stream_events_utils.rs | 2 +- codex-rs/core/tests/suite/items.rs | 61 ++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/stream_events_utils.rs b/codex-rs/core/src/stream_events_utils.rs index 265d7b450fea..f3e3bbc046f0 100644 --- a/codex-rs/core/src/stream_events_utils.rs +++ b/codex-rs/core/src/stream_events_utils.rs @@ -579,7 +579,7 @@ pub(crate) async fn finalize_turn_item( } } if let TurnItem::ImageGeneration(image_item) = &mut *turn_item - && image_item.status == "completed" + && !image_item.result.is_empty() { persist_image_generation_item(sess, turn_context, image_item).await; } diff --git a/codex-rs/core/tests/suite/items.rs b/codex-rs/core/tests/suite/items.rs index a39a2b5e059c..eaeabbdcf894 100644 --- a/codex-rs/core/tests/suite/items.rs +++ b/codex-rs/core/tests/suite/items.rs @@ -435,6 +435,67 @@ async fn image_generation_call_event_is_emitted() -> anyhow::Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn terminal_image_generation_call_with_generating_status_is_persisted() -> anyhow::Result<()> +{ + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + + let TestCodex { + codex, + config, + session_configured, + .. + } = test_codex().build(&server).await?; + let call_id = "ig_terminal_generating_status"; + let expected_saved_path = image_generation_artifact_path( + config.codex_home.as_path(), + &session_configured.thread_id.to_string(), + call_id, + ); + let _ = std::fs::remove_file(&expected_saved_path); + + let response = sse(vec![ + ev_response_created("resp-1"), + ev_image_generation_call(call_id, "generating", "A tiny blue square", "Zm9v"), + ev_completed("resp-1"), + ]); + mount_sse_once(&server, response).await; + + codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "generate a tiny blue square".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + responsesapi_client_metadata: None, + additional_context: Default::default(), + thread_settings: Default::default(), + }) + .await?; + + let end = wait_for_event_match(&codex, |ev| match ev { + EventMsg::ImageGenerationEnd(event) => Some(event.clone()), + _ => None, + }) + .await; + + assert_eq!(end.call_id, call_id); + assert_eq!(end.status, "generating"); + assert_eq!(end.revised_prompt, Some("A tiny blue square".to_string())); + assert_eq!(end.result, "Zm9v"); + assert_eq!( + end.saved_path.as_ref().map(AbsolutePathBuf::as_path), + Some(expected_saved_path.as_path()) + ); + assert_eq!(std::fs::read(&expected_saved_path)?, b"foo"); + let _ = std::fs::remove_file(&expected_saved_path); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn image_generation_call_event_is_emitted_when_image_save_fails() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); From 9227857dd084ae3fd7551f7f82a1dba0c70d78fd Mon Sep 17 00:00:00 2001 From: won Date: Tue, 16 Jun 2026 21:23:15 -0700 Subject: [PATCH 2/3] Simplify image generation persistence coverage --- codex-rs/core/src/stream_events_utils.rs | 4 +- codex-rs/core/tests/suite/items.rs | 65 +----------------------- 2 files changed, 5 insertions(+), 64 deletions(-) diff --git a/codex-rs/core/src/stream_events_utils.rs b/codex-rs/core/src/stream_events_utils.rs index f3e3bbc046f0..7c1e97eb1fbd 100644 --- a/codex-rs/core/src/stream_events_utils.rs +++ b/codex-rs/core/src/stream_events_utils.rs @@ -559,6 +559,7 @@ pub(crate) async fn finalize_turn_item( turn_item: &mut TurnItem, plan_mode: bool, ) { + let should_persist_image = matches!(&contributor_policy, TurnItemContributorPolicy::Run(_)); if let TurnItemContributorPolicy::Run(turn_store) = contributor_policy { apply_turn_item_contributors(sess, turn_store, turn_item).await; } @@ -578,7 +579,8 @@ pub(crate) async fn finalize_turn_item( agent_message.memory_citation = memory_citation; } } - if let TurnItem::ImageGeneration(image_item) = &mut *turn_item + if should_persist_image + && let TurnItem::ImageGeneration(image_item) = &mut *turn_item && !image_item.result.is_empty() { persist_image_generation_item(sess, turn_context, image_item).await; diff --git a/codex-rs/core/tests/suite/items.rs b/codex-rs/core/tests/suite/items.rs index eaeabbdcf894..491891f15d7b 100644 --- a/codex-rs/core/tests/suite/items.rs +++ b/codex-rs/core/tests/suite/items.rs @@ -348,7 +348,7 @@ async fn web_search_item_is_emitted() -> anyhow::Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn image_generation_call_event_is_emitted() -> anyhow::Result<()> { +async fn image_generation_call_with_generating_status_is_persisted() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; @@ -369,7 +369,7 @@ async fn image_generation_call_event_is_emitted() -> anyhow::Result<()> { let first_response = sse(vec![ ev_response_created("resp-1"), - ev_image_generation_call(call_id, "completed", "A tiny blue square", "Zm9v"), + ev_image_generation_call(call_id, "generating", "A tiny blue square", "Zm9v"), ev_completed("resp-1"), ]); mount_sse_once(&server, first_response).await; @@ -421,67 +421,6 @@ async fn image_generation_call_event_is_emitted() -> anyhow::Result<()> { assert!(started.1 > 0); assert_eq!(completed.0.id, call_id); assert!(completed.1 > 0); - assert_eq!(end.call_id, call_id); - assert_eq!(end.status, "completed"); - assert_eq!(end.revised_prompt, Some("A tiny blue square".to_string())); - assert_eq!(end.result, "Zm9v"); - assert_eq!( - end.saved_path.as_ref().map(AbsolutePathBuf::as_path), - Some(expected_saved_path.as_path()) - ); - assert_eq!(std::fs::read(&expected_saved_path)?, b"foo"); - let _ = std::fs::remove_file(&expected_saved_path); - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn terminal_image_generation_call_with_generating_status_is_persisted() -> anyhow::Result<()> -{ - skip_if_no_network!(Ok(())); - - let server = start_mock_server().await; - - let TestCodex { - codex, - config, - session_configured, - .. - } = test_codex().build(&server).await?; - let call_id = "ig_terminal_generating_status"; - let expected_saved_path = image_generation_artifact_path( - config.codex_home.as_path(), - &session_configured.thread_id.to_string(), - call_id, - ); - let _ = std::fs::remove_file(&expected_saved_path); - - let response = sse(vec![ - ev_response_created("resp-1"), - ev_image_generation_call(call_id, "generating", "A tiny blue square", "Zm9v"), - ev_completed("resp-1"), - ]); - mount_sse_once(&server, response).await; - - codex - .submit(Op::UserInput { - items: vec![UserInput::Text { - text: "generate a tiny blue square".into(), - text_elements: Vec::new(), - }], - final_output_json_schema: None, - responsesapi_client_metadata: None, - additional_context: Default::default(), - thread_settings: Default::default(), - }) - .await?; - - let end = wait_for_event_match(&codex, |ev| match ev { - EventMsg::ImageGenerationEnd(event) => Some(event.clone()), - _ => None, - }) - .await; - assert_eq!(end.call_id, call_id); assert_eq!(end.status, "generating"); assert_eq!(end.revised_prompt, Some("A tiny blue square".to_string())); From 07d3c2057d5e5568373181ff6d52181c214cb453 Mon Sep 17 00:00:00 2001 From: won Date: Tue, 16 Jun 2026 22:03:05 -0700 Subject: [PATCH 3/3] Simplify image generation persistence condition --- codex-rs/core/src/stream_events_utils.rs | 4 +--- codex-rs/core/tests/suite/items.rs | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/codex-rs/core/src/stream_events_utils.rs b/codex-rs/core/src/stream_events_utils.rs index 7c1e97eb1fbd..f3e3bbc046f0 100644 --- a/codex-rs/core/src/stream_events_utils.rs +++ b/codex-rs/core/src/stream_events_utils.rs @@ -559,7 +559,6 @@ pub(crate) async fn finalize_turn_item( turn_item: &mut TurnItem, plan_mode: bool, ) { - let should_persist_image = matches!(&contributor_policy, TurnItemContributorPolicy::Run(_)); if let TurnItemContributorPolicy::Run(turn_store) = contributor_policy { apply_turn_item_contributors(sess, turn_store, turn_item).await; } @@ -579,8 +578,7 @@ pub(crate) async fn finalize_turn_item( agent_message.memory_citation = memory_citation; } } - if should_persist_image - && let TurnItem::ImageGeneration(image_item) = &mut *turn_item + if let TurnItem::ImageGeneration(image_item) = &mut *turn_item && !image_item.result.is_empty() { persist_image_generation_item(sess, turn_context, image_item).await; diff --git a/codex-rs/core/tests/suite/items.rs b/codex-rs/core/tests/suite/items.rs index 491891f15d7b..515d496b8b7a 100644 --- a/codex-rs/core/tests/suite/items.rs +++ b/codex-rs/core/tests/suite/items.rs @@ -348,7 +348,7 @@ async fn web_search_item_is_emitted() -> anyhow::Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn image_generation_call_with_generating_status_is_persisted() -> anyhow::Result<()> { +async fn builtin_image_generation_call_persisted() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await;