From 13151988537de2cb930183576710f9b59326aabc Mon Sep 17 00:00:00 2001 From: Won Park Date: Tue, 16 Jun 2026 23:03:00 -0700 Subject: [PATCH] [codex] Persist built-in image results reported as generating (#28656) ## Why #27920 stopped persisting image-generation items unless their status was `completed`, preventing failed standalone extension items with empty results from being saved. Built-in image generation can instead emit a terminal `response.output_item.done` containing a complete base64 PNG while the item status remains `generating`. In that case, app-server emits no `savedPath`, so Codex Apps can render the inline image but cannot expose a file artifact. ## What changed - Persist image-generation items whenever `result` contains image data. Failed terminal items still have empty results and remain unpersisted. - Update the existing built-in image-generation integration test to cover a terminal `generating` item and verify both `saved_path` and the written PNG bytes. ## Validation - Confirmed with a raw built-in websocket trace: the image progressed through `in_progress`, `generating`, and `partial_image`, then emitted one `response.output_item.done` with `status: "generating"` and a complete PNG result. - `just test -p codex-core builtin_image_generation_call_persisted` is currently blocked before test execution by a pre-existing compile error in `thread-store/src/thread_metadata_sync.rs:171`. --- codex-rs/core/src/stream_events_utils.rs | 2 +- codex-rs/core/tests/suite/items.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/codex-rs/core/src/stream_events_utils.rs b/codex-rs/core/src/stream_events_utils.rs index 265d7b450..f3e3bbc04 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 a39a2b5e0..515d496b8 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 builtin_image_generation_call_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; @@ -422,7 +422,7 @@ async fn image_generation_call_event_is_emitted() -> anyhow::Result<()> { 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.status, "generating"); assert_eq!(end.revised_prompt, Some("A tiny blue square".to_string())); assert_eq!(end.result, "Zm9v"); assert_eq!(