Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Route prepared images through the attachment store (#46065)
## What changed

Upload prepared images from messages and tool outputs through the session's `AttachmentStore` before recording history. Use the returned inline bytes or file reference, and fall back to the prepared inline image if upload fails.

Keep history replay on `InlineAttachmentStore` so it does not upload recorded images, and preserve existing file references. Reuse image resizing directly for guardian review comparisons without uploading.

## Testing

Add coverage for upload failure preserving resized inline images. Extend rollout coverage to verify that only new images are uploaded, uploaded bytes have the expected dimensions, and file references and resize notices survive replay.

GitOrigin-RevId: e9966297c3579ef1e52376f0235fe3a7e3ec7ecc
  • Loading branch information
kchainani-oai authored and copyberry committed Sep 17, 2026
commit 5e636ea760d1c821a9162af652a14225fdbd8a0f
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/attachment-store/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ workspace = true

[dependencies]
serde = { workspace = true, features = ["derive"] }
tracing = { workspace = true }

[dev-dependencies]
pretty_assertions = { workspace = true }
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/attachment-store/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ impl fmt::Debug for UploadResult {
pub struct InlineAttachmentStore;

impl AttachmentStore for InlineAttachmentStore {
#[tracing::instrument(level = "trace", skip_all)]
fn upload(&self, request: UploadRequest) -> UploadFuture<'_> {
Box::pin(async move {
Ok(UploadResult::Inline {
Expand All @@ -166,6 +167,7 @@ impl AttachmentStore for InlineAttachmentStore {
})
}

#[tracing::instrument(level = "trace", skip_all)]
fn resolve<'a>(&'a self, request: ResolveRequest<'a>) -> ResolveFuture<'a> {
let file_id = request.file_id;
Box::pin(async move {
Expand Down
41 changes: 16 additions & 25 deletions codex-rs/core/src/guardian/review_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub(crate) use setup::run_guardian_review_session;
mod context_policy;
use context_policy::ReviewContextPolicy;

use std::borrow::Cow;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::PathBuf;
Expand Down Expand Up @@ -68,8 +69,7 @@ use crate::context::GuardianNodeReplPolicy;
use crate::context_manager::ContextManager;
use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::image_preparation::ImagePreparationMode;
use crate::image_preparation::ImageResizeNoticeMode;
use crate::image_preparation::prepare_response_items;
use crate::image_preparation::resize_image;
use crate::image_preparation::unified_image_budget_enabled;
use crate::session::SessionIo;
use crate::session::session::Session;
Expand All @@ -80,7 +80,6 @@ use codex_model_provider_info::ModelProviderInfo;
use codex_protocol::turn_input::TurnInputMode;
use codex_protocol::turn_input::TurnInputRequest;
use codex_protocol::turn_input::TurnInputSubmission;
use codex_protocol::user_input::UserInput;
use codex_thread_store::PersistContext;
use codex_tools::normalize_output_image_detail;
use codex_utils_path_uri::PathUri;
Expand Down Expand Up @@ -486,7 +485,8 @@ async fn run_review_on_session(

let parent_history = params.parent_history.conversation_history_snapshot();
let history = if GuardianContextMode::from_history(parent_history.as_ref())
== GuardianContextMode::ThreadOwned {
== GuardianContextMode::ThreadOwned
{
parent_history
} else {
params.parent_session.conversation_history_snapshot().await
Expand Down Expand Up @@ -515,7 +515,10 @@ async fn run_review_on_session(
_ => &[],
})
.filter_map(|item| match item {
ContentItem::InputImage { image: ImageReference::Inline { image_url }, .. } => Some(image_url.as_str()),
ContentItem::InputImage {
image: ImageReference::Inline { image_url },
..
} => Some(image_url.as_str()),
_ => None,
})
.collect::<HashSet<_>>();
Expand Down Expand Up @@ -547,27 +550,15 @@ async fn run_review_on_session(
Some(ImageDetail::Low) => Some(ImageDetail::High),
detail => detail,
};
let mut prepared = vec![
ResponseInputItem::from(vec![UserInput::Image {
image: ImageReference::Inline {
image_url: image_url.to_owned(),
},
detail: *detail,
}])
.into(),
];
prepare_response_items(
&mut prepared,
mode,
ImageResizeNoticeMode::Disabled,
);
let Some(ResponseItem::Message { content, .. }) = prepared.first() else {
return false;
let prepared_image_url = match resize_image(image_url, detail, mode) {
Ok(Some(prepared)) => Cow::Owned(prepared.into_data_url()),
Ok(None) => Cow::Borrowed(image_url),
Err(error) => {
warn!(%error, "failed to prepare guardian review image");
return false;
}
};
content.iter().any(|item| {
matches!(item, ContentItem::InputImage { image: ImageReference::Inline { image_url }, .. }
if !reviewer_image_urls.contains(image_url.as_str()))
})
!reviewer_image_urls.contains(prepared_image_url.as_ref())
});
let prompt: ResponseItem =
ResponseInputItem::from(prompt_items.context.clone().into_user_inputs()?)
Expand Down
Loading
Loading