+
    jHàj,  ã                  ó  a € 0 t $ R t^ RIHt ^ RIt^ RIt^ RIt^ RIt^ RIt^ RI	H	t	H
t
Ht ^ RIHt ^ RIHtHt ^ RIHt ^ RIHt ^ RIHt ]P.                  ! ]4      tR	tR
 R ltR R lt]P:                  ! R4      tR R lt] ! 0 R/m4      t!R R lt"Rt#Rt$Rs%R]&R&   ]PN                  ! ^R7      R R l4       t(R0R R llt)RRRRR R/R! R" llt*R R/R# R$ llt+R% R& lt,R' R( lt-R R/R) R* llt.R R/R+ R, llt/R R/R- R. llt0R# )1u  External drain-control marker contract (dashboard â†’ gateway).

No control channel exists into a running gateway, so begin/cancel-drain writes
(or removes) ``{HERMES_HOME}/.drain_request.json`` and a gateway watcher reacts;
an ACTIVE marker means ``gateway_state -> "draining"``.  Two lenient staleness
signals (either suffices): epoch mismatch (HERMES_HOME is a durable volume on
Hermes Cloud, so a marker survives the restart a drain-gated action ends in and
would park the fresh gateway in ``draining`` forever) and expiry (same-epoch
orphan past :data:`DRAIN_REQUEST_MAX_AGE_SECONDS`; re-writing refreshes it).
Reading never raises: a malformed file reads as ``{}`` â€” still drain-active
(fail-safe toward quiescing).  Staleness rejects only on a *definite* verdict.
)ÚannotationsN)ÚdatetimeÚtimezoneÚUTC)ÚPath)ÚAnyÚOptional)Ú
_parse_iso)Úget_hermes_home)Úatomic_json_writeÚrestart_waitc               ó   € V ^8„  d   QhRR/# )é   ÚreturnÚNone© )Úformats   "Ú:/home/ubuntu/.hermes/hermes-agent/gateway/drain_control.pyÚ__annotate__r   $   s   € ÷ 9ñ 9 ñ 9ó    c                óp   € \        V RR4      pVe%   VP                  R4      '       g   \        VR&   R# R# R# )z^Publish the restart's after-turn wait (``drain_reason``) unless a drain already named its own.Úsession_runtime_descriptorNÚdrain_reason)ÚgetattrÚgetÚRESTART_WAIT_REASON©ÚrunnerÚ
descriptors   & r   Úopen_restart_waitr   $   s8   € ä˜Ð!=¸tÓD€JØÒ j§n¡n°^×&DÒ&DÜ%8ˆ
�>Ó"ñ 'EÑr   c               ó   € V ^8„  d   QhRR/# ©r   r   Úboolr   )r   s   "r   r   r   +   s   € ÷ Gñ G ñ Gr   c                óØ   € \        V RR4      ;'       g    / p\        \        V RR4      4      ;'       d7    \        \        V RR4      4      ;'       d    VP                  R4      \        8H  # )zSTrue while *runner* is in a restart's after-turn wait (draining, not yet stopping).r   NÚ	_drainingFÚ_runningr   )r   r"   r   r   r   s   & r   Úrestart_wait_openr&   +   sk   € ä˜Ð!=¸tÓD×JÐJÈ€JÜ”˜ ¨eÓ4Ó5÷ Fð F¼$¼wÀvÈzÐ[`Ó?aÓ:b÷ Fð FØ—‘˜~Ó.Ô2EÑEðGr   zh/api/(?:health|status|profiles/sessions(?:/sidebar)?|sessions/[^/]+(?:/messages(?:/around)?|/timeline)?)c               ó   € V ^8„  d   QhRR/# r!   r   )r   s   "r   r   r   ;   s   € ÷ +ñ +¨tñ +r   c                ó’   € VR8H  ;'       d<    \        \        P                  T;'       g    R4      4      ;'       d    \        V 4      # )zVTrue for an allowlisted read request while *runner* is in a restart's after-turn wait.ÚGETÚ )r"   Ú_RESTART_WAIT_READSÚ	fullmatchr&   )r   ÚmethodÚpaths   &&&r   Úrestart_wait_readr/   ;   sC   € à�e‰O÷ *ð *¤Ô%8×%BÑ%BÀ4Ç:À:È2Ó%NÓ O÷ *ð *Ü! &Ó)ð+r   c               ó   € V ^8„  d   QhRR/# r!   r   )r   s   "r   r   r   L   s   € ÷ _ñ _¸ñ _r   c                óP   € \        \        V RR4      4      ;'       d
    V\        9  # )zUTrue when an interactive WebSocket verb must be refused because *runner* is draining.r$   F)r"   r   ÚRESTART_WAIT_WS_METHODS)r   r-   s   &&r   Ú ws_method_refused_while_drainingr3   L   s$   € ä”˜ ¨UÓ3Ó4×^Ð^¸ÔG^Ñ9^Ð^r   z.drain_request.jsong      ¬@zOptional[str]Ú_expiry_logged_for)Úmaxsizec               ó   € V ^8„  d   QhRR/# )r   r   Ústrr   )r   s   "r   r   r   \   s   € ÷ Hñ H Sñ Hr   c                 ó  € R;r\         P                  ! \        4      ;_uu_ 4        \        R4      P	                  RR7      P                  4       p RRR4       \         P                  ! \        \        4      ;_uu_ 4        \        R4      P	                  RR7      P                  R^4      ^,          P                  4       ^,          pRRR4       V '       g	   V'       d   V  RV 2# R#   + '       g   i     L ; i  + '       g   i     L<; i)	u|  Identity of THIS container / VM instantiation ("<boot_id>:<pid1_start>").

Stable for the life of PID 1 (a gateway-only respawn keeps honouring an
in-flight drain) but changes when the machine is recreated: boot_id on a VM
reboot, PID 1's start time on ``docker restart``.  ``""`` when neither is
readable (non-Linux, no ``/proc``) disables the epoch check â€” never fail-closed.
r*   z/proc/sys/kernel/random/boot_idzutf-8©ÚencodingNz/proc/1/statÚ)Ú:)	Ú
contextlibÚsuppressÚOSErrorr   Ú	read_textÚstripÚ
IndexErrorÚrsplitÚsplit)Úboot_idÚ
pid1_starts     r   Úcurrent_instantiation_epochrG   [   sÅ   € ð Ð€GÜ	×	Ò	œW×	%Õ	%ÜÐ8Ó9×CÑCÈWÐCÓU×[Ñ[Ó]ˆ÷ 
&ä	×	Ò	œW¤j×	1Õ	1ô ˜.Ó)×3Ñ3¸WÐ3ÓE×LÑLÈSÐRSÓTÐUVÕW×]Ñ]Ó_Ð`bÕcˆ
÷ 
2÷ *1·JˆgˆY�a˜
�|Ð$ÐGÀRÐG÷ 
&×	%úç	1×	1ús   ¦*C(Á?AC;Ã(C8	Ã;D	c               ó    € V ^8„  d   QhRRRR/# )r   ÚhomeúOptional[Path]r   r   r   )r   s   "r   r   r   n   s   € ÷ [ñ [˜^ð [°tñ [r   c                óh   € \        V e   V 4      \        ,          # \        4       4      \        ,          # )zBAbsolute path to the drain-request marker, respecting HERMES_HOME.)r   r
   Ú_DRAIN_REQUEST_FILENAME©rI   s   &r   Údrain_request_pathrN   n   s)   € ä˜Ò(�Ó@ÔCZÕZÐZ¬oÓ.?Ó@ÔCZÕZÐZr   Ú	principalzdrain-controlÚsuppress_notificationFrI   c               ó(   € V ^8„  d   QhRRRRRRRR/# )	r   rO   r7   rP   r"   rI   rJ   r   údict[str, Any]r   )r   s   "r   r   r   s   s,   € ÷ ñ ØðØ@DðØTbðàñr   c                ó¶   € RRR\         P                  ! \        4      P                  4       RV R\	        4       R\        V4      /p\        \        V4      V4       V# )a³  Write the begin-drain marker atomically; returns the payload.

Re-writing refreshes ``requested_at`` (keep-alive past the max-age).
``suppress_notification`` skips ONLY the home-channel "gateway shutting down"
broadcast (the per-session interrupt ping is never suppressed); which drains
are quiet is the caller's policy.  Stamped with the instantiation epoch so a
copy surviving a machine restart on the durable volume reads as stale.
ÚactionÚdrainÚrequested_atrO   ÚepochrP   )r   Únowr   Ú	isoformatrG   r"   r   rN   )rO   rP   rI   Úpayloads   $$$ r   Úwrite_drain_requestr[   s   sV   € ð 	�'˜>¬8¯<ª<¼Ó+<×+FÑ+FÓ+HÈ+ÐW`ØÔ,Ó.Ð0GÌÐNcÓIdð€Gô Ô(¨Ó.°Ô8Ø€Nr   c               ó    € V ^8„  d   QhRRRR/# ©r   rI   rJ   r   r"   r   )r   s   "r   r   r   †   s   € ÷ 	ñ 	 ð 	¸4ñ 	r   c                óÈ   € \        V 4      p VP                  4        R#   \         d8   p\        T\        4      '       g   \
        P                  RY4        Rp?R# Rp?ii ; i)zPRemove the drain marker (cancel-drain, idempotent). Returns True if one existed.Tz&drain-control: failed to remove %s: %sNF)rN   Úunlinkr?   Ú
isinstanceÚFileNotFoundErrorÚ_logÚwarning)rI   r.   Úes   $  r   Úclear_drain_requestre   †   sM   € ä˜dÓ#€DðØ�‰ŒÙøÜô Ü˜!Ô.×/Ò/Ü�L‰LÐAÀ4ÔKÝûðús   � ŸA!ª,AÁA!c               ó    € V ^8„  d   QhRRRR/# )r   ÚbodyrR   r   r"   r   )r   s   "r   r   r   ’   s   € ÷ ñ ˜^ð °ñ r   c           	     ó2  € V P                  R4      p\        V4      pVf   R# \        P                  ! \        4      V,
          P                  4       pV\        8:  d   R# \        V8w  d.   Vs\        P                  RW\        V P                  R4      4       R# )uè   True iff ``requested_at`` parses AND is older than the max-age.

Missing/unparseable and future-dated (clock skew) timestamps are honoured.
Logged once per marker, not per poll â€” the operator's breadcrumb for a leak.

See #85433.
rV   FuÌ   drain-control: ignoring expired drain marker (requested_at=%s, age=%.0fs > max %.0fs, principal=%s) â€” the drain that wrote it was never cancelled; treating as stale so the gateway keeps accepting turns.rO   T)
r   r	   r   rX   r   Útotal_secondsÚDRAIN_REQUEST_MAX_AGE_SECONDSr4   rb   rc   )rg   ÚrawrV   Úages   &   r   Ú_marker_is_expiredrm   ’   s‚   € ð �(‰(�>Ó
"€CÜ˜c“?€LØÒÙÜ�<Š<œÓ˜|Õ+×
:Ñ
:Ó
<€CØ
Ô+Ô+ÙÜ˜SÔ Ø ÐÜ�‰ðwàÔ3°T·X±X¸kÓ5Jô	
ñ
 r   c               ó    € V ^8„  d   QhRRRR/# ©r   rI   rJ   r   zOptional[dict[str, Any]]r   )r   s   "r   r   r   ¬   s   € ÷ ñ ˜^ð Ð0Hñ r   c                ó°   € \        V R7      pVf   R# \        4       VP                  R4      r2V'       d   V'       d   W28w  g   \        V4      '       d   R# V# )zUMarker body if present AND not stale (definite epoch mismatch or expired), else None.rM   NrW   )Úread_drain_requestrG   r   rm   )rI   rg   ÚcurrentÚmarker_epochs   &   r   Ú_active_drain_bodyrt   ¬   sE   € ä 4Ô(€DØ‚|ÙÜ7Ó9¸4¿8¹8ÀGÓ;Lˆ\ß—L \Ô%<ÔASÐTX×AYÒAYÙØ€Kr   c               ó    € V ^8„  d   QhRRRR/# r]   r   )r   s   "r   r   r   ·   s   € ÷ 0ñ 0˜^ð 0°tñ 0r   c                ó   € \        V 4      RJ# )ug  True iff an active (present, same-epoch, unexpired) begin-drain marker exists.

A marker whose ``epoch`` does not match the current instantiation epoch is treated as absent: it
survived a container/VM restart (HERMES_HOME is a durable Fly volume on Hermes Cloud) and the lifecycle
action that triggered the drain has already completed â€” honouring it would wedge the freshly-restarted
gateway in ``draining`` (NS-570). A marker whose ``requested_at`` is older than
:data:`DRAIN_REQUEST_MAX_AGE_SECONDS` is likewise treated as absent: it is a same-epoch orphan whose
drain-gated action completed without a restart and was never cancelled (#85433). Both staleness checks
are lenient (see :func:`_marker_epoch_is_stale` / :func:`_marker_is_expired`): a legacy/corrupt marker
with no epoch and no timestamp, or an environment without ``/proc``, still reads as drain-active.
N)rt   rM   s   $r   Údrain_requestedrw   ·   s   € ô ˜dÓ#¨4Ð/Ð/r   c               ó    € V ^8„  d   QhRRRR/# r]   r   )r   s   "r   r   r   Æ   s   € ÷ <ñ <¨>ð <ÀTñ <r   c                ó`   € \        V 4      p\        T;'       d    VP                  R4      4      # )u¦  True iff an ACTIVE marker asks to suppress the shutdown broadcast.

Same activeness rule as :func:`drain_requested`, so an orphan can never silence
a fresh gateway; a marker without the field reads False (fail toward louder).

"Active" means exactly what :func:`drain_requested` means â€” a marker present AND stamped with the
current instantiation epoch AND not past its max-age. A stale (other-epoch) marker that survived a
machine restart on the durable HERMES_HOME volume, or an expired same-epoch orphan (#85433), is ignored
here just as it is for drain state (NS-570): we must never let an orphaned marker's flag silence a
*fresh* gateway's legitimate shutdown broadcast.
rP   )rt   r"   r   )rI   rg   s   $ r   Údrain_notification_suppressedrz   Æ   s+   € ô ˜dÓ#€DÜ�×:Ð:˜Ÿ™Ð"9Ó:Ó;Ð;r   c               ó    € V ^8„  d   QhRRRR/# ro   r   )r   s   "r   r   r   Ö   s   € ÷ 2ñ 2 ð 2Ð:Rñ 2r   c                ó\  € \        V 4      p VP                  RR7      p \        P                  ! T4      p\        T\        4      '       d   T# / #   \         d8   p\        T\        4      '       g   \
        P                  RY4        Rp?R# Rp?ii ; i  \        \        3 d    / u # i ; i)z_Return the marker payload, ``{}`` if present but unparseable, ``None`` if absent. Never raises.z	utf-8-sigr9   z$drain-control: failed to read %s: %sN)rN   r@   r?   r`   ra   rb   rc   ÚjsonÚloadsÚ
ValueErrorÚ	TypeErrorÚdict)rI   r.   rk   rd   Údatas   $    r   rq   rq   Ö   s˜   € ä˜dÓ#€DðØ�n‰n kˆnÓ2ˆð
Ü�zŠz˜#‹ˆô ˜d¤D×)Ò)ˆ4Ð1¨rÐ1øô ô Ü˜!Ô.×/Ò/Ü�L‰LÐ?ÀÔIÝûðûô œ	Ð"ô ØŠ	ðús(   �A  B ÁBÁ,BÂBÂB+Â*B+>   Úpingúclarify.lockúsession.infoúsession.listúprompt.submitúprompt.receiptúsession.detachúsession.resumeúclarify.respondúapproval.pendingúapproval.respondúruntime.describeúapproval.receivedúsession.interruptúsession.events.since)N)1Ú__conditional_annotations__Ú__doc__Ú
__future__r   r=   Ú	functoolsr}   ÚloggingÚrer   r   r   Úpathlibr   Útypingr   r   Úgateway.memory_statusr	   Úhermes_constantsr
   Úutilsr   Ú	getLoggerÚ__name__rb   r   r   r&   Úcompiler+   r/   Ú	frozensetr2   r3   rL   rj   r4   Ú__annotations__Ú	lru_cacherG   rN   r[   re   rm   rt   rw   rz   rq   )r’   s   @r   Ú<module>r£      s*  øðôõ #ã Û Û Û Û 	ß ,Ñ ,Ý ß  å ,Ý ,Ý #à×Ò˜Ó"€ð
 %Ð õ9õGð —j’jð<ó=Ð õ
+ñ $ò %ó Ð õ_ð 0Ð ð
 !'Ð ð %)Ð �MÓ (ð ×Ò˜QÔôHó  ðH÷$[ð
Ø'ðØGLðØei÷ð&	°$÷ 	õõ4ð0¨d÷ 0ð<¸D÷ <ð 2°÷ 2ñ 2r   