fix(agent): accept pixel-correct image downscale when bytes grow (#48013) - #49124
fix(agent): accept pixel-correct image downscale when bytes grow (#48013)#49124alt-glitch wants to merge 1 commit into
Conversation
) The image-too-large reactive shrink (try_shrink_image_parts_in_messages) conflated two independent constraints: it always rejected a resize whose re-encoded bytes were >= the original, even when the shrink was driven by a PIXEL-DIMENSION cap (Anthropic many-image 2000px) rather than the byte budget. Downscaled screenshot PNGs routinely re-encode LARGER in bytes, so the dimension-correct result was discarded and the image left oversized -> the provider re-rejected on retry and the session wedged forever. Fix: track which constraint triggered the shrink (bytes vs dimension) and gate the accept on the SAME axis. * dimension path: accept the result as long as it is now within max_dimension, regardless of byte size (verify via Pillow; fall back to the byte gate only when the re-encode can't be decoded). * bytes path: still require bytes to shrink, but ALSO re-check the per-side cap when it's active — _resize_image_for_vision returns a best-effort, possibly over-cap blob when it exhausts its halving budget on a very-high-aspect image, so a byte-shrink alone can leave it over the dimension cap and re-brick on retry. Extend the unshrinkable-oversized guard to the pixel axis so a partial shrink doesn't burn the one-shot retry. Single shared agent path -> fixes CLI, TUI, and gateway alike. Adds a real-Pillow runnable proof (repro_48013_image_shrink_brick.py) that reproduces the issue's per-image table (bricks 3/5 before, passes 5/5 after) plus unit invariants for the dimension and bytes accept/reject paths, partial-progress accounting, and the bytes-path still-over-cap regression surfaced by adversarial review. Closes #48013
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-import |
2 |
invalid-assignment |
1 |
First entries
tests/run_agent/repro_48013_image_shrink_brick.py:41: [unresolved-import] unresolved-import: Cannot resolve imported module `PIL`
tests/run_agent/repro_48013_image_shrink_brick.py:33: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/run_agent/test_credits_notices_toggle.py:76: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to attribute `_credits_session_start_micros` of type `int`
✅ Fixed issues (2):
| Rule | Count |
|---|---|
unresolved-attribute |
2 |
First entries
run_agent.py:2971: [unresolved-attribute] unresolved-attribute: Object of type `Self@get_credits_spent_micros` has no attribute `_credits_session_start_micros`
tests/run_agent/test_credits_notices_toggle.py:76: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_credits_session_start_micros` on type `AIAgent`
Unchanged: 5846 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
|
Duplicate of #48035 — both fix #48013 with the same mechanism in |
|
Local validation on PR head python -m pytest tests/run_agent/test_image_shrink_recovery.py -q -o 'addopts='
# 21 passed, 1 warning
python -m py_compile agent/conversation_compression.py tests/run_agent/test_image_shrink_recovery.py
# passedThe implementation and tests look sound in isolation, but this now appears superseded by |
|
Call-graph-assisted review (calldiff over ✅ Looks good
🟡 Minor
🔴 Staleness — likely needs a rebase before mergeMerge-base is 2026-06-19; |
Summary
Fixes the image-dimension 400 session brick (#48013). The reactive image-shrink recovery (
try_shrink_image_parts_in_messages) silently discarded a pixel-correct downscale whenever the re-encoded PNG was larger in bytes than the original — the common case for downscaled Retina screenshots. The image was left at its original oversized dimensions, the provider re-rejected it on the one-shot retry, and the session wedged forever on the Anthropic many-image 2000px path (the defaultanthropic/openrouter→anthropicroute).The recovery machinery already parses the correct ceiling (#45979) and
_resize_image_for_visionalready produces the correct downscale — the defect was one layer down, in the accept/reject gate:The success criterion was byte size, but the failing constraint was pixel dimensions. The two were conflated.
Fix
Track which constraint triggered the shrink (
bytesvsdimension) and gate the accept on the same axis:max_dimension, regardless of byte size (verify via Pillow; fall back to the byte gate only when the re-encode can't be decoded)._resize_image_for_visionreturns a best-effort, possibly-over-cap blob when it exhausts its halving budget on a very-high-aspect image (it freezes the long side once the short side hits its 64px floor), so a byte-shrink alone can leave it over the dimension cap and re-brick on retry.unshrinkable_oversizedguard to the pixel axis so a partial shrink doesn't burn the one-shot retry.Single shared agent path → fixes CLI, TUI, and gateway alike (all surfaces go through the same
try_shrink_image_parts_in_messagescall site inagent/conversation_loop.py).Proof
tests/run_agent/repro_48013_image_shrink_brick.pyis a runnable, real-Pillow fixture (no mocks) that reproduces the issue's exact per-image table:Stashing the fix flips it to
FAIL: 3 image(s) still over the pixel cap (BRICK)(exit 1) — it's a discriminating proof, not a tautology.Tests
tests/run_agent/test_image_shrink_recovery.py(21 tests, all green): dimension accept-on-byte-growth, dimension-failure-blocks-retry, mixed partial-progress accounting, and the bytes-path still-over-cap regression (verified to fail without the fix). Full related compression suite (123 tests) passes.Relationship to the other open PRs
main; it predates themax_dimensionplumbing entirely and its one-line change would silently revert the 2000px-cap parsing from fix: shrink images to provider dimension limit #45979.mainand additionally closes the bytes-path gap (a byte-shrunk-but-still-over-cap image), surfaced by a 3-reviewer adversarial pass that all converged on it.Closes #48013