The floor cut-back (`step > total - floor` -> `step = total - floor`) fires whenever an
eight-carrier batch is wider than the fit window: tool results carrying several images,
or user uploads filling most of the ceiling. `total - floor` tracks the total, so the
retire count stopped being a step function and the frontier moved on every turn again —
the per-image prefix rewrite this policy exists to prevent.
Replay over 24 turns (prefix collapses; issue #113517 script):
carrier shape before after
1 image / tool_result 2 2
2 images / tool_result 3 3
3 images / tool_result 13 7
4 images / tool_result 16 11
15 uploads + 1 / tool_result 15 10
The quantum is now `min(batch, window - floor)` where `window` is how many newest
carriers fit. Each advance keeps at least `floor + 1` frames, so the cut-back clause is
gone; holding for `window - floor` turns per advance is the most the floor allows. The
one-image case (#113517's report) is unchanged: 21/29/37 -> 8/16/24 retired.
Regression test: test_frontier_holds_when_tool_results_carry_several_images — red on
the previous loop shape, green here.
The compressor pass and the Anthropic wire pass each carried their own copy of the
limit/batch/floor constants (under different names) and of the retire-count loop.
Retuning one alone would make the wire pass re-evict on a second frontier after the
compressor pass — the exact prefix-rewrite class #113517 fixes.
- agent/image_eviction_policy.py: stdlib-only leaf holding the constants and
outbound_image_retire_count(); the wire converter stays a leaf (no compaction import).
- evict_stale_outbound_tool_images loses `keep_newest`: no caller passed it, and its
default was the COMPACTION window constant the comments say the send path must not use.
- _retire_stale_tool_result_images (compaction) back to its main body; behaviour was
unchanged, so the rewrite was churn.
- One `_tool_result_parts` unwrap instead of three copies; one `_image_payload` walk
per message instead of two.
- Wire pass counts blocks via `_block_type` (non-dict inner blocks no longer raise).
Tests import the constants from the policy module; the two "stops at the floor" tests
assert the invariant (newest floor survives, request under the limit) instead of the
exact frontier. test_user_uploads_are_not_evicted now builds a request that actually
trips eviction — at 6 blocks it never fired and proved nothing.