§
    øžyj"-  ã                  óþ   — d Z ddlmZ ddlZddlZddlmZ ddlmZ ddl	m
Z
mZmZmZmZmZ dZdZd	Zd
ZdZ ed¬¦  «         G d„ d¦  «        ¦   «         Zd+d„Zd,d„Zd-d„Zd-d„Zd.d„Zd/d„Zd-d„Zd0d „Zdd!œd1d*„ZdS )2uï  Memory-pressure bounds for the gateway's per-session AIAgent cache.

The gateway caches one ``AIAgent`` per session so a long-lived conversation
reuses its prompt prefix instead of rebuilding the system prompt every turn.
Each cached agent also pins ``_session_messages`` â€” the full live transcript,
tool outputs included, which is tens of MB on a tool-heavy session.

``gateway/run.py`` bounds that cache two ways, and both are blind to how much
memory it actually holds:

* the LRU cap counts *entries*, not bytes, and 128 warm transcripts is
  several GB;
* the idle TTL only sheds agents that went quiet for an hour, and it
  deliberately defers eviction for a finalizable session that has not expired
  yet, so a busy gateway hoards every transcript all day.

This module supplies the missing signal: the process's own anonymous RSS,
compared against a budget derived from the cgroup limit the gateway actually
runs under.  ``GatewayRunner._sweep_agent_cache_under_pressure`` uses it to
shed LRU transcripts through the existing soft-eviction path, which rebuilds
from the persisted session on the next turn (#80764).

Everything here is pure or read-only so it can be tested without a gateway.
Config lives under ``agent.agent_cache`` in ``config.yaml``.
é    )ÚannotationsN)Ú	dataclass)ÚPath)ÚAnyÚCallableÚIterableÚListÚOptionalÚTuplegÍÌÌÌÌÌä?i   é   é   i   T)Úfrozenc                  óZ   — e Zd ZU dZdZded<   dZded<   dZded<   eZ	ded	<   e
Zded
<   dS )ÚAgentCacheBoundsa!  Operator-facing bounds for the per-session agent cache.

    ``max_size`` and ``idle_ttl_secs`` are ``None`` when the operator did not
    set them, so ``gateway/run.py`` keeps using its module-level defaults.
    ``memory_high_mb`` is ``None`` when pressure eviction is switched off.
    NúOptional[int]Úmax_sizeúOptional[float]Úidle_ttl_secsÚmemory_high_mbÚintÚmax_evictions_per_passÚprotect_recent)Ú__name__Ú
__module__Ú__qualname__Ú__doc__r   Ú__annotations__r   r   Ú_DEFAULT_MAX_EVICTIONS_PER_PASSr   Ú_DEFAULT_PROTECT_RECENTr   © ó    úB/home/ragecks/.hermes/hermes-agent/gateway/agent_cache_pressure.pyr   r   6   st   € € € € € € ðð ð #€HÐ"Ð"Ð"Ñ"Ø%)€MÐ)Ð)Ð)Ñ)Ø$(€NÐ(Ð(Ð(Ñ(Ø"AÐÐAÐAÐAÑAØ1€NÐ1Ð1Ð1Ñ1Ð1Ð1r!   r   Úvaluer   Úreturnr   c                óš   — t          | t          ¦  «        s| €d S 	 t          | ¦  «        }n# t          t          f$ r Y d S w xY w|dk    r|nd S ©Nr   )Ú
isinstanceÚboolr   Ú	TypeErrorÚ
ValueError©r#   Úparseds     r"   Ú_positive_intr-   F   sh   € Ý�%�ÑÔð  % -ØˆtðÝ�U‘”ˆˆøÝ•zÐ"ð ð ð Øˆtˆtðøøøà˜a’Z�Zˆ6ˆ6 TÐ)ó   ›+ «A ¿A r   c                óš   — t          | t          ¦  «        s| €d S 	 t          | ¦  «        }n# t          t          f$ r Y d S w xY w|dk    r|nd S r&   )r'   r(   Úfloatr)   r*   r+   s     r"   Ú_positive_floatr1   P   sh   € Ý�%�ÑÔð  % -ØˆtðÝ�u‘”ˆˆøÝ•zÐ"ð ð ð Øˆtˆtðøøøà˜a’Z�Zˆ6ˆ6 TÐ)r.   c                 óú  — t           j        dk    rdS g } 	 ddlm}  |¦   «         }n# t          $ r d}Y nw xY w|r%|dk    r|                      d|› d�d|› d�f¦  «         |                      d	¦  «         | D ]ƒ}	 t          |¦  «                             d
¬¦  «                             ¦   «         }n# t          $ r Y ŒEw xY w|r|dk    rŒR	 t          |¦  «        }n# t          $ r Y Œow xY w|dk    s|dk    rŒ€|c S dS )u  Return the memory limit this process runs under, if it is cgroup-capped.

    Prefers cgroup v2 ``memory.high`` (the throttling point â€” passing it is
    what stalled the reported shutdown) over ``memory.max``, and falls back to
    cgroup v1.  ``max`` / absurd sentinel values mean "unlimited".

    Checks the process's *own* cgroup first (where a systemd unit's
    ``MemoryHigh=``/``MemoryMax=`` actually lands â€” the root files read
    ``max`` on those deployments), then walks up to the root for
    container-style limits.
    ÚlinuxNr   )Ú_own_cgroup_pathú/z/sys/fs/cgroupz/memory.highz/memory.max)z/sys/fs/cgroup/memory.highz/sys/fs/cgroup/memory.maxz+/sys/fs/cgroup/memory/memory.limit_in_byteszutf-8)ÚencodingÚmaxl            )ÚsysÚplatformÚgateway.cgroup_cleanupr4   Ú	ExceptionÚextendr   Ú	read_textÚstripÚOSErrorr   r*   )Ú
candidatesr4   ÚownÚ	candidateÚrawÚlimits         r"   Ú_cgroup_limit_bytesrE   Z   s™  € õ „|�wÒÐØˆtØ€JðØ;Ð;Ð;Ð;Ð;Ð;àÐÑ Ô ˆˆøÝð ð ð Øˆˆˆðøøøà
ð 
ˆs�cŠzˆzØ×Òà2 Ð2Ð2Ð2Ø1 Ð1Ð1Ð1ðñ	
ô 	
ð 	
ð ×Òð	
ñô ð ð  ð ð ˆ	ð	Ý�y‘/”/×+Ò+°WÐ+Ñ=Ô=×CÒCÑEÔEˆCˆCøÝð 	ð 	ð 	ØˆHð	øøøàð 	�c˜U’l�lØð	Ý˜‘H”HˆEˆEøÝð 	ð 	ð 	ØˆHð	øøøð �AŠ:ˆ:˜ 7Ò+Ð+ØØˆˆˆØˆ4s0   –' §6µ6Á:5B0Â0
B=Â<B=Ã
CÃ
C'Ã&C'c                 ó<  — 	 t          t          j        d¦  «        ¦  «        t          t          j        d¦  «        ¦  «        z  S # t          t          t
          f$ r Y nw xY w	 dd l} t          |                      ¦   «         j        ¦  «        S # t          $ r Y d S w xY w)NÚSC_PAGE_SIZEÚSC_PHYS_PAGESr   )
r   ÚosÚsysconfr?   r*   ÚAttributeErrorÚpsutilÚvirtual_memoryÚtotalr;   )rL   s    r"   Ú_total_memory_bytesrO   �   sª   € ðÝ•2”:˜nÑ-Ô-Ñ.Ô.µµR´ZÀÑ5PÔ5PÑ1QÔ1QÑQÐQøÝ•Z¥Ð0ð ð ð ØˆðøøøðØˆˆˆå�6×(Ò(Ñ*Ô*Ô0Ñ1Ô1Ð1øÝð ð ð Øˆtˆtðøøøs$   ‚AA ÁAÁAÁ#)B Â
BÂBÚsettingc                óž  — t          | t          ¦  «        rB|                      ¦   «                              ¦   «         }|dk    r|dv rdnt	          |¦  «        S n)t          | t
          ¦  «        r| sdS nt	          | ¦  «        S t          ¦   «         pt          ¦   «         }|sdS t          |t          z  t          z  ¦  «        }|t          k    r|ndS )ax  Resolve the ``memory_high_mb`` setting into an absolute MB budget.

    ``"auto"`` derives a budget from the cgroup limit the gateway runs under
    (or total RAM when uncapped), which is what makes this fix work out of the
    box on the containerised/systemd deployments where the leak bites.  A
    positive number is taken literally; anything falsy disables the pass.
    Úauto)Ú ÚoffÚnoneÚfalseÚdisabledN)r'   Ústrr>   Úlowerr-   r(   rE   rO   r   Ú_AUTO_BUDGET_FRACTIONÚ_BYTES_PER_MBÚ_AUTO_BUDGET_FLOOR_MB)rP   Ú
normalizedrD   Úbudgets       r"   Úresolve_memory_high_mbr_   œ   së   € õ �'�3ÑÔð &Ø—]’]‘_”_×*Ò*Ñ,Ô,ˆ
Ø˜ÒÐð Ð!IÐIÐIð �å" :Ñ.Ô.ðð  õ 
�G�TÑ	"Ô	"ð &Øð 	Ø�4ð	õ ˜WÑ%Ô%Ð%åÑ!Ô!Ð:Õ%8Ñ%:Ô%:€EØð ØˆtÝ�Õ.Ñ.µÑ>Ñ?Ô?€FØÕ4Ò4Ð4ˆ6ˆ6¸$Ð>r!   Úconfigc           
     óÞ  — d}t          | t          ¦  «        r?|                      d¦  «        }t          |t          ¦  «        r|                     d¦  «        }t          |t          ¦  «        si }t          |                     d¦  «        ¦  «        }|                     d¦  «        }t          |¦  «        }|€2t          |t          ¦  «        rt          |t
          ¦  «        s|dk    rd}t          t          |                     d¦  «        ¦  «        t          |                     d¦  «        ¦  «        t          |                     d	d
¦  «        ¦  «        |�|nt          |�|nt          ¬¦  «        S )a  Read ``agent.agent_cache`` out of a raw config mapping.

    Reads the *raw* user config (the gateway's loader does not deep-merge
    ``DEFAULT_CONFIG``), so an absent key stays absent and the caller can tell
    "operator chose 128" from "operator said nothing".
    NÚagentÚagent_cacher   r   r   r   r   r   rR   )r   r   r   r   r   )r'   ÚdictÚgetr-   r   r(   r   r1   r_   r   r   )r`   ÚsectionÚ	agent_cfgÚmax_evictionsr   Úprotect_parseds         r"   Úresolve_agent_cache_boundsrj   ¹   sX  € ð €GÝ�&�$ÑÔð 3Ø—J’J˜wÑ'Ô'ˆ	Ý�i¥Ñ&Ô&ð 	3Ø—m’m MÑ2Ô2ˆGÝ�g�tÑ$Ô$ð Øˆå! '§+¢+Ð.FÑ"GÔ"GÑHÔH€MØ—[’[Ð!1Ñ2Ô2€NÝ" >Ñ2Ô2€NàÐÝ�~¥sÑ+Ô+ð 	å˜>­4Ñ0Ô0ð 	ð ˜aÒÐð
 ˆåÝ˜wŸ{š{¨:Ñ6Ô6Ñ7Ô7Ý% g§k¢k°/Ñ&BÔ&BÑCÔCÝ-¨g¯kªkÐ:JÈFÑ.SÔ.SÑTÔTà*Ð6ˆMˆMÕ<[ð -Ð8ˆNˆNÕ>Uð
ñ 
ô 
ð 
r!   c                 óê  — 	 ddl m}   | ¦   «         }|                     d¦  «        }t          |t          ¦  «        r|dk    r|dz  S |                     d¦  «        }t          |t          ¦  «        r|dk    r|dz  S n# t
          $ r Y nw xY w	 ddl}t	          |                     t          j	        ¦   «         ¦  «         
                    ¦   «         j        t          z  ¦  «        S # t
          $ r Y dS w xY w)u—  Return the process's anonymous resident memory in MB, or None.

    Anonymous pages are the ones cached transcripts live in â€” the reported
    incident measured 11.0 GB of anon out of 11.0 GB total, so file-backed
    pages are noise here.  ``collect_memory_snapshot`` already reads
    ``/proc/self/status`` without a dependency; psutil covers everything else,
    where only total RSS is available.
    r   )Úcollect_memory_snapshotÚrss_anon_kibi   Úrss_kibN)Úhermes_cli.mem_trimrl   re   r'   r   r;   rL   ÚProcessrI   ÚgetpidÚmemory_infoÚrssr[   )rl   ÚsnapshotÚanon_kibrn   rL   s        r"   Úread_anon_rss_mbrv   ã   s  € ðØ?Ð?Ð?Ð?Ð?Ð?à*Ð*Ñ,Ô,ˆØ—<’< Ñ/Ô/ˆÝ�h¥Ñ$Ô$ð 	$¨°Aª¨Ø˜tÑ#Ð#Ø—,’,˜yÑ)Ô)ˆÝ�g�sÑ#Ô#ð 	#¨°!ª¨Ø˜d‘?Ð"øøÝð ð ð ØˆðøøøðØˆˆˆå�6—>’>¥"¤)¡+¤+Ñ.Ô.×:Ò:Ñ<Ô<Ô@Å=ÑPÑQÔQÐQøÝð ð ð Øˆtˆtðøøøs+   ‚AA= Á4A= Á=
B
Â	B
ÂAC$ Ã$
C2Ã1C2rb   r(   c                óò   — t          | dd¦  «        }t          |t          ¦  «        sdS t          | dd¦  «        }t          |t          ¦  «        rt          |t          ¦  «        rdS |t          |¦  «        k    S )ux  True when the agent's live transcript is fully on disk.

    Soft eviction drops ``_session_messages`` and rebuilds it from the
    persisted session next turn, so it is only safe once persistence has
    caught up.  ``_last_flushed_db_idx`` is advanced to ``len(messages)`` by
    ``AIAgent._flush_messages_to_session_db`` and only on a fully successful
    write â€” the same divergence the FTS write-corruption guard reacts to when
    it preserves live history over a lagging transcript.  Unknown shapes are
    treated as *not* caught up: a skipped eviction costs memory, a wrong one
    costs the user their conversation.
    Ú_session_messagesNFÚ_last_flushed_db_idx)Úgetattrr'   Úlistr   r(   Úlen)rb   ÚmessagesÚflusheds      r"   Ú transcript_persistence_caught_upr     sx   € õ �uÐ1°4Ñ8Ô8€HÝ�h¥Ñ%Ô%ð ØˆuÝ�eÐ3°TÑ:Ô:€GÝ�g�sÑ#Ô#ð ¥z°'½4Ñ'@Ô'@ð ØˆuØ•c˜(‘m”mÒ#Ð#r!   )r   Úordered_entriesúIterable[Tuple[str, Any]]Úis_evictableúCallable[[str, Any], bool]rh   r   r   úList[Tuple[str, Any]]c               ó2  — t          | ¦  «        }|dk    s|sg S t          t          |d¦  «        t          |¦  «        dz  ¦  «        }|r|d| …         }g }|D ]=\  }}t          |¦  «        |k    r n$ |||¦  «        r|                     ||f¦  «         Œ>|S )u�  Choose which cached sessions to shed, least-recently-used first.

    ``ordered_entries`` must be in LRUâ†’MRU order (the cache is an
    ``OrderedDict`` kept in that order by ``move_to_end`` on every hit).  The
    batch is capped so one pass cannot stall the gateway tearing down clients.

    ``protect_recent`` is an upper bound, clamped to half the cache: a handful
    of sessions can be big enough to exhaust the budget on their own (a single
    tool-heavy transcript runs to hundreds of MB), and a fixed guard would
    then protect the entire cache and leave the gateway climbing toward the
    OOM killer with nothing it is willing to shed.
    r   é   N)r{   Úminr7   r|   Úappend)	r€   r‚   rh   r   ÚentriesÚprotectÚplanÚkeyrb   s	            r"   Úplan_pressure_evictionsr�     sÀ   € õ& �?Ñ#Ô#€GØ˜ÒÐ ÐØˆ	Ý•#�n aÑ(Ô(­#¨g©,¬,¸!Ñ*;Ñ<Ô<€GØð %Ø˜)˜G˜8˜)Ô$ˆà"$€DØð &ð &‰
ˆˆUÝˆt‰9Œ9˜Ò%Ð%ØˆEØˆ<˜˜UÑ#Ô#ð 	&Ø�KŠK˜˜e˜Ñ%Ô%Ð%øØ€Kr!   )r#   r   r$   r   )r#   r   r$   r   )r$   r   )rP   r   r$   r   )r`   r   r$   r   )rb   r   r$   r(   )
r€   r�   r‚   rƒ   rh   r   r   r   r$   r„   )r   Ú
__future__r   rI   r8   Údataclassesr   Úpathlibr   Útypingr   r   r   r	   r
   r   rZ   r\   r   r   r[   r   r-   r1   rE   rO   r_   rj   rv   r   r�   r    r!   r"   ú<module>r’      s½  ððð ð4 #Ð "Ð "Ð "Ð "Ð "à 	€	€	€	Ø 
€
€
€
Ø !Ð !Ð !Ð !Ð !Ð !Ø Ð Ð Ð Ð Ð Ø AÐ AÐ AÐ AÐ AÐ AÐ AÐ AÐ AÐ AÐ AÐ AÐ AÐ AÐ AÐ Að Ð ð Ð à"$Ð ð Ð à€ð €�$ÐÑÔð2ð 2ð 2ð 2ð 2ñ 2ô 2ñ Ôð2ð*ð *ð *ð *ð*ð *ð *ð *ð2ð 2ð 2ð 2ðj
ð 
ð 
ð 
ð?ð ?ð ?ð ?ð:'ð 'ð 'ð 'ðTð ð ð ð<$ð $ð $ð $ð4 ð ð  ð  ð  ð  ð  ð  ð  r!   