[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`.
This commit is contained in:
Won Park
2026-06-16 23:03:00 -07:00
committed by GitHub
Unverified
parent 172b2218a5
commit 1315198853
2 changed files with 4 additions and 4 deletions
+1 -1
View File
@@ -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;
}
+3 -3
View File
@@ -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!(