Skip to content

fix(converter): store the bytes of every Codex tool output image the port names - #42

Merged
laithalsaadoon merged 1 commit into
mainfrom
factory/7-codex-mcp-image-blobs
Oct 8, 2026
Merged

laithalsaadoon merged 1 commit into
mainfrom
factory/7-codex-mcp-image-blobs

Conversation

@laithalsaadoon

Copy link
Copy Markdown
Owner

fix(converter): store the bytes of every Codex tool output image the port names

Follow-up to #41. The harbor 0.24.0 port turns an inline image in a Codex tool output list into a blob placeholder ([image sha256:<hex> image/png <n> bytes]) through a throwaway BlobCollector(), but the pre-pass extract_codex_blobs lifted only strict input_image data URLs. An MCP image block, {"type": "image", "data": <base64>, "mimeType": ...}, got a placeholder naming a sha256 the corpus blob store never received. The same was true of an input_image in a tool output whose data URL carries parameters (data:image/png;charset=x;base64,...) or whose base64 is wrapped across lines: the port accepts both, the pre-pass's strict match did not.

What changed

  • domain/blobs.py: codex_tool_image_ref(data, media_type, collector) is the one rule for an inline Codex tool output image, ported from harbor's Codex._image_media_type / _save_tool_output_image: png, jpeg, gif or webp (image/jpg read as image/jpeg), a data URL's own media type winning over mimeType, whitespace removed before strict base64 decoding. CODEX_TOOL_IMAGE_MEDIA_TYPES and codex_tool_image_media_type moved here from codex_conversion.py.
  • The pre-pass lifts, from function_call_output and custom_tool_call_output lists only: an MCP image block by that rule (mimeType, else mime_type), and an input_image the strict data URL match misses. The collector stores the bytes and the index maps them to the output's call_id, so the result gains typed extra.images.
  • codex_conversion._inline_image_text calls the same function, so the placeholder text and the stored blob agree byte for byte.
  • Unchanged: message content (an image block there is not lifted, since the port reads only text fields there), and every image the port calls [image omitted] (unsupported type, bad base64, no media type, a remote URL), which stays in the record untouched.

Schema version

CONVERTER_SCHEMA_VERSION 7 -> 8, digest re-pinned. Placeholder text is byte-identical, but artifacts change for sessions with such images: their results gain extra.images, their bytes reach the blob store, and a tool output list the port dumps as JSON (one element of an unknown type) holds the placeholder item where it held the base64. A bump re-converts the whole corpus on the next materialize.

Gates

  • mise run check: green. 2418 passed, 1 skipped in 279.83s; ruff lint and format, ty, pyright (0 errors), import-linter (7 kept, 0 broken), actionlint, lefthook validate.
  • Parity: test_parity_codex.py, test_harbor_oracle.py, test_parity_live.py: 10 passed, 1 skipped (the golden re-freeze helper). _image_content_forgiven is untouched.
  • New packages/atif-converter/tests/test_codex_blobs.py, 22 tests: MCP image lifted and indexed in both output kinds; mime_type key, jpg alias, data URL and data URL parameters, wrapped base64; 8 unlifted cases (bmp, text/plain, bad base64, empty, no media type, remote URL, non-string data, no data); message content untouched; input_image with parameters or wrapped base64 lifted in a tool output but not in message content; end to end through convert_codex_and_audit, each placeholder's sha256 is a blob in result.blobs whose bytes hash to it; raw and pre-passed records convert to the same trajectory.

Planted defects

  • Tests on main's src (git stash of both source files): 11 of 21 new tests fail.
  • MCP branch disabled in _codex_image_ref: 9 fail. input_image fallback disabled: 2 fail.
  • Reverting blobs.py alone breaks the import of codex_tool_image_ref in codex_conversion.py, so collection fails.

Risks and what to look at

  • The input_image fallback goes beyond the plan's MCP-only scope. It closes the same unresolved-placeholder gap for the port's wider data URLs and changes no placeholder text.
  • A tool output that is a JSON string encoding a list is still not lifted: the port renders placeholders for it but the pre-pass rewrites only list outputs. No local rollout has an MCP image at all (2 rollouts carry mimeType, both text/plain resources), so this case is unmeasured.
  • The pre-pass's strict input_image path still lifts types the port omits (for example image/bmp), as on main.

🤖 Generated with Claude Code

…port names

The harbor 0.24.0 port turns an inline image in a Codex tool output list into
a blob placeholder through a throwaway BlobCollector, but the pre-pass
(extract_codex_blobs) lifted only strict input_image data URLs. An MCP image
block ({"type": "image", "data": <base64>, "mimeType": ...}), and an
input_image whose data URL carries parameters or whose base64 is wrapped, got
a placeholder naming a sha256 the corpus blob store never received.

- blobs.codex_tool_image_ref is now the one rule both sides use: harbor's
  supported image types (image/jpg read as image/jpeg), a data URL's own
  media type winning, whitespace removed before strict decoding. The port's
  _inline_image_text calls it; the pre-pass lifts a tool output's MCP image
  by it, and an input_image the strict match misses, so the collector holds
  the bytes and the index maps them to the output's call_id.
- Message content is unchanged: an image block there is not lifted.
- An image the port calls [image omitted] (unsupported type, bad base64, no
  media type, a remote URL) stays in the record untouched.

Placeholder text is byte-identical, so the parity oracle and its
_image_content_forgiven rule are untouched. CONVERTER_SCHEMA_VERSION goes to
8: affected results gain extra.images, their bytes reach the blob store, and
a list the port dumps as JSON holds the placeholder item where it held the
base64. Digest re-pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@laithalsaadoon
laithalsaadoon merged commit 25bda72 into main Oct 8, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant