+
    iHàjl7  ã                  óú   € R t ^ RIHt ^ RIt^ RIt^ RIHtHtHtH	t	 ^t
^dtRt^t^tRtR t]! 0 R!m4      tR R ltR R	 ltR
 R ltR R ltR R ltRRR^ RRRRR]R]R]/R R lltR R ltR R ltR R ltR# )"ub
  Send-path image eviction policy shared by both stateless outbound passes.

Two passes retire old tool-result images from the per-request copy of the conversation:
``agent.context_compressor.evict_stale_outbound_tool_images`` on the OpenAI-shaped list and
``agent.anthropic_message_convert._evict_old_screenshots`` on the Anthropic wire list. Both run
from scratch on a fresh clone every request, so they must agree on one policy or the second
pass re-evicts on a different frontier than the first (#113517). This module is stdlib-only so
the wire converter, a leaf, can import it without dragging in the compaction stack.

Why the trigger is a provider limit, not a keep-newest count: retiring an image edits a message
the provider has already cached, and Anthropic matches its prompt cache on an exact byte prefix.
A keep-newest-N window retires one more message on every new image, so every turn is a
full-prefix miss. Holding images until the request would cross a real API limit and then
retiring a batch costs one slower turn per batch and nothing below the limit.

20 is the documented threshold at which Anthropic applies a stricter per-image dimension cap
(2000 px) to EVERY image in the request, counting images nested in tool_result content. Crossing
it costs nothing by itself: it only matters when some image actually exceeds 2000 px on a side.
Hermes already shrinks its own tool images far below that before they enter history
(``vision_analyze`` and browser screenshots embed at 1568 px, ``computer_use`` at 1456 px by
default), so the 21st screenshot of a run trips no provider constraint and a batch retire there
buys a full prefix rewrite for nothing â€” and, on the preserved-thinking models, invalidates every
later thinking block. Anthropic's own computer-use guidance now says to keep screenshots at or
under 2000 px and NOT to prune them client-side for exactly that reason.

So the block ceiling is CONDITIONAL on the request's actual dimensions: the strict 20 applies
only when some image exceeds ``MANY_IMAGE_DIMENSION_LIMIT``, when the stricter per-side cap is
live and the retire genuinely protects the request; otherwise the ceiling is the hard count
``OUTBOUND_IMAGE_HARD_LIMIT``. The hard ceilings above 20 are real but distant (100 images per
request on 200K-context models, 600 otherwise) and the 32 MB request-size limit usually binds
first, which the byte budget guards with headroom for text. Classification reads image HEADERS
only â€” never a full decode of every base64 payload per request â€” and treats unreadable or unknown
dimensions as unsafe, so a payload this module cannot measure keeps today's strict trigger.
)ÚannotationsN)ÚAnyÚIterableÚOptionalÚSequencei 6niÐ  c               ó    € V ^8„  d   QhRRRR/# )é   ÚrawÚbytesÚreturnzOptional[tuple[int, int]]© )Úformats   "Ú@/home/ubuntu/.hermes/hermes-agent/agent/image_eviction_policy.pyÚ__annotate__r   ?   s   € ÷ ñ  Uð Ð/Hñ ó    c                óš  € V P                  R4      '       dG   \        V 4      ^8¼  d7   \        P                  ! RV R,          4      w  r\	        V4      \	        V4      3# V P                  R4      '       EdV   \        V 4      ^8”  EdE   ^pV^	,           \        V 4      8  Ed+   W,          ^ÿ8w  d   V^,          pK0  W^,           ,          V^,           r4V^ÿ8X  d#   V\        V 4      8  d   W,          V^,           r4K)  VR9   d   Ky  V^Ú8X  g   V^,           \        V 4      8”  d    R# \        P                  WV^,            R4      pV^8  g   W5,           \        V 4      8”  d    R# V\        9   dT   V^8¼  dM   \        P                  W^,           V^,            R4      \        P                  W^,           V^,            R4      3# W5,          pEKB  R# )z÷(width, height) for PNG / JPEG bytes, or None when unreadable. PNG: IHDR. JPEG: walk
segments (skipping 0xFF fill bytes) to the first SOF marker; stop at SOS. Lives here so both
the send-path passes and the tool layer share one header-only reader.s   ‰PNG

z>II:é   é   Ns   ÿØÚbigN>   éØ   éÙ   )Ú
startswithÚlenÚstructÚunpackÚintÚ
from_bytesÚ_JPEG_SOF_MARKERS)r	   ÚwidthÚheightÚiÚmarkerÚsegment_lens   &     r   Úimage_dimensions_from_bytesr#   ?   sv  € ð ‡~�~Ð*×+Ò+´°C³¸B´ÜŸš e¨S°­ZÓ8‰ˆÜ�5‹zœ3˜v›;Ð&Ð&Ø
‡~�~�k×"Ó"¤s¨3£x°!¥|ØˆØ�!�e”c˜#“hÕØ�v˜Œ~Ø�Q•�ÙØ �E�
 A¨¥E�AØ˜D”. Q¬¨S«¤\Ø�F A¨¥EšØ˜Ô%ÙØ˜Œ~  Q¥¬¨S«Ô!1Øñ ô Ÿ.™.¨¨q°1­u¨°uÓ=ˆKØ˜QŒ !¥/´C¸³HÔ"<Øñ ð Ô*Ô*¨{¸aÔ/?Ü—~‘~ c¨a­%°°AµÐ&6¸Ó>ÄÇÁÈsÐWXÕSXÐYZÐ]^ÕY^ÐO_ÐafÓ@gÐgÐgØÕ‹AÙr   c               ó    € V ^8„  d   QhRRRR/# )r   ÚdataúOptional[str]r   úOptional[int]r   )r   s   "r   r   r   \   s   € ÷ 3ñ 3 =ð 3°]ñ 3r   c                óà  € V '       g   R# T pVP                  R4      '       d8   VP                  R4      w  r#pV'       d   RV RV P                  R4       9  d   R# VP                  4       pVR\         pVR\        V4      \        V4      ^,          ,
           pV'       g   R#  \        P                  ! VRR7      p\        T4      pTe   \        T4      # R#   \        \        3 d     R# i ; i)a“  Longest side (px) of a data-URL / bare-base64 image, or None when it cannot be read.

Decodes only the leading base64 prefix (``_DIMENSION_PROBE_CHARS``), so classifying a request
with dozens of images costs a few header reads, not a full decode of every payload. Accepts
both ``data:image/...;base64,....`` and a bare base64 string; anything else (a remote URL, an
unrecognized container) yields None.
Nzdata:Ú,z;base64F)Úvalidate)r   Ú	partitionÚindexÚstripÚ_DIMENSION_PROBE_CHARSr   Úbase64Ú	b64decodeÚ
ValueErrorÚ	TypeErrorr#   Úmax)r%   ÚpayloadÚ_Ú	separatorÚprefixr	   Údimss   &      r   Úimage_max_side_from_datar9   \   sß   € ÷ ÙØ€GØ×Ñ˜'×"Ò"Ø '× 1Ñ 1°#Ó 6Ñˆ�gß˜I¨TÐ2B°4·:±:¸c³?Ð-CÔCÙØ�m‰m‹o€GØÐ,Ô,Ð-€FØÐ5”c˜&“k¤S¨£[°1¥_Õ5Ð6€FßÙðÜ×Ò˜v°Ô6ˆô ' sÓ+€DØÒ(Œ3ˆt‹9Ð2¨dÐ2øô œ	Ð"ô Úðús   Â$C ÃC-Ã,C-c               ó    € V ^8„  d   QhRRRR/# )r   r%   r&   r   Úboolr   )r   s   "r   r   r   x   s   € ÷ Iñ I¨ð I¸$ñ Ir   c                óB   € \        V 4      pVRJ;'       d
    V\        8*  # )a  True only when the image's longest side is known and within ``MANY_IMAGE_DIMENSION_LIMIT``.

Unknown dimensions are NOT safe: the stricter per-request cap would still be live, so the
caller keeps the strict trigger. See :func:`images_all_within_many_image_limit`.
N)r9   ÚMANY_IMAGE_DIMENSION_LIMIT)r%   Úlongests   & r   Úimage_within_many_image_limitr?   x   s'   € ô ' tÓ,€GØ˜$Ð×HÐH 7Ô.HÑ#HÐHr   c               ó    € V ^8„  d   QhRRRR/# )r   ÚdataszIterable[Optional[str]]r   r;   r   )r   s   "r   r   r   ‚   s   € ÷ 
ñ 
Ð.Eð 
È$ñ 
r   c                ó@   € V  F  p\        V4      '       d   K   R# 	  R# )an  True when EVERY image in the request is a readable image within the many-image limit.

The Anthropic rule is per request: one oversized image holds every image in the request to the
stricter cap, so a single unreadable or over-limit image must keep the strict block ceiling.
Short-circuits on the first offender, which is what keeps this cheap on image-heavy turns.
FT)r?   )rA   r%   s   & r   Ú"images_all_within_many_image_limitrC   ‚   s!   € ó ˆÜ,¨T×2Ô2Úñ ñ r   c               ó    € V ^8„  d   QhRRRR/# )r   Úmany_image_safer;   r   r   r   )r   s   "r   r   r   �   s   € ÷ Rñ R°ð R¸ñ Rr   c                ó*   € V '       d   \         # \        # )z[Block ceiling for a request: the strict 20 unless every image is within the many-image cap.)ÚOUTBOUND_IMAGE_HARD_LIMITÚOUTBOUND_IMAGE_LIMIT)rE   s   &r   Úoutbound_image_block_limitrI   �   s   € ç(7Ô$ÐQÔ=QÐQr   Úcarrier_bytes_newest_firstÚreserved_bytesrE   FÚlimitÚbudgetÚbatchÚfloorc               ó@   € V ^8„  d   QhRRRRRRRRRR	R
RRRRRRRRR/
# )r   Úcarrier_blocks_newest_firstúSequence[int]Úreserved_blocksr   rJ   úOptional[Sequence[int]]rK   rE   r;   rL   r'   rM   rN   rO   r   r   )r   s   "r   r   r   ”   sl   € ÷ :ñ :Ø!.ð:àð:ð !8ð	:ð
 ð:ð ð:ð ð:ð ð:ð ð:ð ð:ð 	ñ:r   c               ó¬   a aaaaaaa	a
€ Sf   \        V4      o\        S 4      o
Vo	\        VVV VVVVV	V
3	R l\        ^S
^,           4       4       ^ R7      # )u3  How many of the OLDEST image-bearing tool results to retire.

``carrier_blocks_newest_first`` is the image-block count per image-bearing tool result,
newest first; ``reserved_blocks`` counts images the pass must never rewrite (user
uploads). The byte dimension is active only when ``carrier_bytes_newest_first`` is
given. ``limit`` defaults to :func:`outbound_image_block_limit` of ``many_image_safe``,
so both passes pick the same ceiling from the same per-request classification.

The retire count must be a STEP FUNCTION of the overshoot, because the pass is recomputed
on every request: an exact ``count - limit`` target moves the frontier on every new image,
and a fixed one-batch retire stops enforcing the limit after the first batch. So the count
advances in quanta until the request fits.

The quantum is ``batch`` capped at ``window - floor``, where ``window`` is how many newest
carriers fit under the ceiling. A quantum wider than that would step past the newest frames
on every advance; cutting each step back to exactly ``total - floor`` instead makes the
retire count track ``total`` again â€” the per-image frontier this policy exists to avoid,
visible whenever a tool result carries several images or uploads fill most of the ceiling.
Holding for ``window - floor`` turns per advance is the most the floor allows.

The floor is a SATISFIABILITY floor: it shelters the newest frames only when reserved
uploads alone breach the block ceiling (no retirement can fix that), and never under byte
pressure â€” the request-size limit is hard and the provider answers 413. Even then the
count stays a step function: it retires whole quanta down toward the floor (keeping
``floor`` to ``floor + batch - 1`` carriers, fewer when the byte budget binds), so the
cached prefix is rewritten once per quantum rather than on every new image.
c              3  ó|   <	"  € T F1  p\        SS
V,
          R  SS	f   R MS	S
V,
          R  SSSSS4      x € K3  	  R # 5i©N)Ú_retire_count_for_request)Ú.0ÚmrN   rM   rQ   rO   rL   rS   rK   ÚsizesÚtotals   & €€€€€€€€€r   Ú	<genexpr>Ú.outbound_image_retire_count.<locals>.<genexpr>À   sZ   øé € ð 	
ñ )�ô &Ø+¨E°A­I¨JÐ7ØØš‘¨5°¸µ°Ð+<ØØØØØ÷	ð 	ó )ùs   ƒ9<)Údefault)rI   r   r3   Úrange)rQ   rS   rJ   rK   rE   rL   rM   rN   rO   r[   r\   s   ff$d$dddd@@r   Úoutbound_image_retire_countra   ”   sW   ÿø€ ðN ‚}Ü*¨?Ó;ˆÜÐ+Ó,€EØ&€EÜ÷	
ô 	
ô ˜1˜e a�iÔ(ó	
ð ôð r   c               ó    € V ^8„  d   QhRRRR/# )r   Úagentr   r   zset[str]r   )r   s   "r   r   r   Ò   s   € ÷ ñ  cð ¨hñ r   c                ó�   € \        V RR4      p\        V RR4      pVe   V^ ,          V8w  d   V\        4       3pW n        V^,          # )a  The session's latched set of tool-call ids whose images the send path has retired.

Keyed on ``agent.session_id`` so a new session (or a compaction rotation, which rewrites the
prefix anyway) starts clean and a reused provider call id cannot retire a fresh screenshot.
Ú
session_idNÚ_outbound_retired_image_ids)ÚgetattrÚsetrf   )rc   re   Úhelds   &  r   Úoutbound_retired_image_idsrj   Ò   sL   € ô ˜ ¨dÓ3€JÜ�5Ð7¸Ó>€DØ‚|�t˜A•w *Ô,ØœC›EÐ"ˆØ,0Ô)Ø��7€Nr   c               ó(   € V ^8„  d   QhRRRRRRRR/# )r   Úretirer   Úcarrier_ids_newest_firstzSequence[Optional[str]]Úretired_idszOptional[set[str]]r   r   )r   s   "r   r   r   à   s,   € ÷ ñ ØðØ+BðØQcðàñr   c                ó  € Vf   V # \        V4      p\        V4       UUu. uF  w  rEV'       g   K  WR9   g   K  VNK  	  pppV'       d   \        WV^ ,          ,
          4      MT p VP                  R WV ,
          R  4       4       V # u uppi )a˜  Widen ``retire`` so every carrier up to the newest previously retired one stays retired,
then record the carriers being retired into ``retired_ids``.

The per-request decision cannot see history: a request that drops an upload (once it leaves
the replay window) relaxes the ceiling, and re-deciding from scratch would restore
screenshots an earlier request already retired, rewriting the cached prefix back.
Nc              3  ó8   "  € T F  q'       g   K  Vx € K  	  R # 5irW   r   )rY   Úcids   & r   r]   Ú'latched_retire_count.<locals>.<genexpr>ï   s   é € ÐWÑ&O˜sÓSV—s’sÓ&Oùs   ‚	�
)r   Ú	enumerater3   Úupdate)rl   rm   rn   r\   Únrq   Úlatcheds   &&&    r   Úlatched_retire_countrw   à   sƒ   € ð ÒØˆÜÐ(Ó)€EÜ(Ð)AÔBÔaÑB‘V�QÄcŒqÈcÑN`�qˆqÑB€GÑaß07ŒS� ¨¥Õ+Ô,¸V€FØ×ÑÑWÐ&>Àv½~¸Ñ&OÓWÔWØ€Mùó bs    B±B¹Bc               ó<   € V ^8„  d   QhRRRRRRRRRRR	RR
RRRRR/	# )r   rQ   rR   rS   r   r[   rT   rK   rL   rM   rN   rO   r   r   )r   s   "r   r   r   ò   sb   € ÷ .ñ .Ø!.ð.àð.ð #ð.ð ð	.ð
 ð.ð ð.ð ð.ð ð.ð 	ñ.r   c                óž  aaaaaaaaa€ \        V 4      p^ .V^,           ,          o^ .V^,           ,          o\        V4       FJ  p	SV	,          W	,          ,           SV	^,           &   SV	,          Se
   SV	,          M^ ,           SV	^,           &   KL  	  R VVVV3R lloR VVVV3R lloS! V4      '       d   ^ # \        \        V^ 4      V4      pS! ^ 4      '       gn   S! V4      '       d`   \        V3R l\        Wx^,           4       4       4      p
\        ^\        WjV,
          ^,           4      4      pW‡,
          pWÌV,          ,
          # S! ^ 4      '       d&   \        V3R l\        V^,           4       4       4      M^ p\        ^\        WmV,
          4      4      p^ pWè8  d,   \        Wë,           V4      pS! WŽ,
          4      '       g   K.   V# V# )zSRetire count for one request in isolation; see :func:`outbound_image_retire_count`.c               ó    € V ^8„  d   QhRRRR/# ©r   Úkeptr   r   r;   r   )r   s   "r   r   Ú/_retire_count_for_request.<locals>.__annotate__  s   € ÷ Lñ L˜ð L ñ Lr   c                óB   <€ SR J ;'       g    SSV ,          ,           S8*  # rW   r   )r|   rM   Ú
bytes_keptrK   r[   s   &€€€€r   Ú
_bytes_fitÚ-_retire_count_for_request.<locals>._bytes_fit  s$   ø€ Ø˜ˆ}×KÐK °¸DÕ1AÕ AÀVÑ KÐKr   c               ó    € V ^8„  d   QhRRRR/# r{   r   )r   s   "r   r   r}     s   € ÷ Qñ Q�Cð Q˜Dñ Qr   c                óJ   <€ SSV ,          ,           S8*  ;'       d	    S! V 4      # rW   r   )r|   r€   Úblocks_keptrL   rS   s   &€€€€r   Ú_fitsÚ(_retire_count_for_request.<locals>._fits  s%   ø€ Ø ¨TÕ!2Õ2°eÑ;×PÐPÁ
È4Ó@PÐPr   c              3  óH   <"  € T F  pS! V4      '       g   K  Vx € K  	  R # 5irW   r   )rY   Úkr€   s   & €r   r]   Ú,_retire_count_for_request.<locals>.<genexpr>  s   øé € ÐNÑ%< Á
È1ÇŸ!š!Ó%<ùó   ƒ"˜
"c              3  óH   <"  € T F  pS! V4      '       g   K  Vx € K  	  R # 5irW   r   )rY   rˆ   r…   s   & €r   r]   r‰     s   øé € Ð9Ñ,�q±°a·—’Ó,ùrŠ   )r   r`   Úminr3   )rQ   rS   r[   rK   rL   rM   rN   rO   r\   r    Úbyte_windowÚquantumÚexcessÚwindowrl   r€   r…   r„   r   s   &fffff&&       @@@@r   rX   rX   ò   s}  ÿø€ ô Ð+Ó,€EØ�#˜ �Õ#€KØ�˜ �	Õ"€JÜ�5Ž\ˆØ(¨�^Ð.IÕ.LÕLˆ�A˜•EÑØ& q�M¸Ò9J¨U°1®XÐPQÕRˆ
�1�q•5Óñ ÷Ló L÷Qó Qñ ˆU‡|‚|Ùä”�E˜1“˜uÓ%€EÙ��8Š8™
 5×)Ò)ô ÔN¤U¨5¸!µ)Ô%<ÓNÓNˆÜ�aœ˜U°%Õ$7¸!Õ$;Ó<Ó=ˆØ•ˆØ Õ(Õ(Ð(Ù=BÀ1¿XºXŒSÔ9œE %¨!¥)Ô,Ó9Ô9È1€FÜ�!”S˜¨¥Ó/Ó0€Gà€FØ
Œ.Ü�VÕ% uÓ-ˆÙ�•× Ô ØØ€Mˆ6€Mr   i €  >   éÀ   éÁ   éÂ   éÃ   éÅ   éÆ   éÇ   éÉ   éÊ   éË   éÍ   éÎ   éÏ   )Ú__doc__Ú
__future__r   r/   r   Útypingr   r   r   r   rH   rG   ÚOUTBOUND_IMAGE_BUDGET_BYTESÚIMAGE_EVICTION_BATCHÚOUTBOUND_IMAGE_FLOORr=   r.   Ú	frozensetr   r#   r9   r?   rC   rI   ra   rj   rw   rX   r   r   r   Ú<module>r¥      sÎ   ðñ!õF #ã Û ß 4Ó 4àÐ ð
  Ð Ø(Ð ØÐ àÐ ð "Ð ð #Ð ÙÒlÓmÐ õõ:3õ8Iõ
õRð
:ð ;?ð	:ð
 ð:ð "ð:ð  ð:ð .ð:ð &ð:ð &÷:õ|õ÷$.r   