+
    jHàj£1  ã                  óà  € R t ^ RIHt ^ RIt^ RIt^ RIt^ RIt^ RIt^ RIt	^ RI
Ht ^ RIHtHtHt ]P                   ! ]4      tRtRtRtRtR	tR
s^t0 R1mt]P6                  P9                  RR4      tR R ltR2R R llt]3R R llt R R lt!R R lt"R R lt#R2R R llt$R2R R llt%R2RR/R R lllt&R2R  R! llt'R2R" R# llt(R2R$ R% llt)R&t*R't+R(t,R)]*R*R/R+ R, llt-R2R-]R)R./R/ R0 lllt.R# )3a–  Scale-to-zero idle detection + dormant-quiesce for the gateway.

Owns the *decision* to go idle, drives the relay transport's ``go_dormant()``, then SUSPENDS the
machine via the local Fly Machines API socket; wake stays platform-side (autostart on wakeUrl).
Self-suspend because Fly Proxy only sees INBOUND proxied connections: it would suspend mid-turn or
before ``go_dormant()`` flipped the relay destination (buffered-event black hole). Enable is gated
SOLELY by the NAS "Labs" toggle env stamp (not config); the idle timeout IS config.yaml. Quiesce
uses ``go_dormant()`` (never disconnect/drain); ``mark_resume_pending`` is NOT called: suspend
preserves RAM.)ÚannotationsN)ÚPath)ÚAnyÚIterableÚOptionalÚHERMES_SCALE_TO_ZEROÚFLY_APP_NAMEÚFLY_MACHINE_IDz	/.fly/apiÚGATEWAY_RELAY_SLEEP_URLFÚstatezdashboard_clients.heartbeatc               ó$   € V ^8„  d   QhRRRRRR/# )é   ÚenvúOptional[dict]ÚkeyÚstrÚreturn© )Úformats   "Ú:/home/ubuntu/.hermes/hermes-agent/gateway/scale_to_zero.pyÚ__annotate__r   .   s&   € ÷ Jñ J�.ð J sð J¨sñ Jó    c                ó|   € \        V f   \        P                  MT P                  VR4      4      P	                  4       # )NÚ )r   ÚosÚenvironÚgetÚstrip)r   r   s   &&r   Ú_env_strr   .   s-   € Ü˜cšk”—
’
¨s×7Ñ7¸¸RÓ@ÓA×GÑGÓIÐIr   c               ó    € V ^8„  d   QhRRRR/# ©r   r   r   r   Úboolr   )r   s   "r   r   r   2   s   € ÷ Cñ C >ð C¸Tñ Cr   c                óL   € \        V \        4      P                  4       \        9   # )zFWhether the Labs toggle stamp is set. Absent/blank/falsey -> disabled.)r   ÚSCALE_TO_ZERO_ENVÚlowerÚ_TRUTHY©r   s   &r   Úscale_to_zero_enabledr'   2   s   € ä�GÔ.Ó/×5Ñ5Ó7¼7ÑBÐBr   c               ó$   € V ^8„  d   QhRRRRRR/# )r   Ú	cfg_valuer   Údefault_minutesÚintr   Úfloatr   )r   s   "r   r   r   7   s(   € ÷ Hñ H¨#ð HØ03ðHØW\ñHr   c                ó�   €  \        V 4      pV^ 8:  d   \        V4      R,          # TR,          #   \        \        3 d    Rp L7i ; i)zŒCoerce ``scale_to_zero.idle_timeout_minutes`` to seconds. Non-numeric / non-positive
degrades to the default (never <= 0: instant dormancy).g        g      N@)r,   Ú	TypeErrorÚ
ValueError)r)   r*   Úminutess   && r   Úparse_idle_timeout_secondsr1   7   sN   € ðÜ˜	Ó"ˆð '.°¤lŒE�/Ó"À4ÕGÐG¸À4ÕGÐGøô ”zÐ"ô ØŠðús   ‚/ ¯AÁAc               ó    € V ^8„  d   QhRRRR/# )r   Ú	platformszIterable[Any]r   r!   r   )r   s   "r   r   r   B   s   € ÷ ñ °ð À4ñ r   c           
     ó¾   € V  Uu0 uF5  p\        \        VRV4      4      P                  4       P                  4       kK7  	  ppVP	                  R4       V'       * # u upi )zÏTrue iff the only connected platform is RELAY, or there is none. A directly-connected
platform holds a live socket and cannot scale to zero. Compared by ``.value``/name so this
module stays enum-import-free.ÚvalueÚrelay)r   Úgetattrr   r$   Údiscard)r3   ÚpÚnamess   &  r   Ú!messaging_is_relay_only_or_absentr;   B   sT   € ñ CLÓLÁ)¸QŒS”˜˜G QÓ'Ó(×.Ñ.Ó0×6Ñ6Ö8Á)€EÐLØ	‡M�M�'ÔØŒ9Ðùò Ms   …;Ac               ó(   € V ^8„  d   QhRRRRRRRR/# )r   Úenabledr!   Úrelay_only_or_absentÚwake_urlúOptional[str]r   r   )r   s   "r   r   r   K   s/   € ÷ Kñ K˜4ð K°tð KÀ}ð KÐY]ñ Kr   c                ód   € \        V 4      ;'       d    \        V4      ;'       d    \        V4      # )záArm only if ALL hold: flag on, relay-only/absent messaging, wakeUrl registered
(a suspended instance with no wake target is a black hole). Otherwise the watcher
never starts, so a non-opted instance behaves exactly as before.)r!   )r=   r>   r?   s   $$$r   Ú
should_armrB   K   s(   € ô �‹=×JÐJœTÐ"6Ó7×JÐJ¼DÀ»NÐJr   c          
     ó,   € V ^8„  d   QhRRRRRRRRRR/# )	r   Úactive_work_countr+   Úseconds_since_last_inboundr,   Úidle_timeout_secondsÚhas_live_background_workr!   r   r   )r   s   "r   r   r   R   s9   € ÷ Dñ D #ð DÀ5ð DØ"'ðDØCGðDØLPñDr   c                óB   € V ^ 8*  ;'       d    V'       * ;'       d    W8¬  # )u]  Pure idle predicate: no active work, no inbound within the window, no live background work.
``active_work_count`` is the BROAD aggregate (agent turns + cron + API runs) â€” passing only
``len(_running_agents)`` reopens the mid-cron-job suspend hole. Callers that cannot read a
work source must fail AWAKE (pass a positive sentinel), never fail to 0.r   )rD   rE   rF   rG   s   $$$$r   Úis_idlerI   R   s6   € ð  Ñ"÷ Cð CÐ+CÔ'C÷ Cð CØ*ÑBðDr   c               ó   € V ^8„  d   QhRR/# )r   Úhermes_homeúOptional[os.PathLike | str]r   )r   s   "r   r   r   \   s   € ÷ >ñ >Ð1Lñ >r   c                óP   € V f   ^ RI Hp V! 4       p \        V 4      \        ,          # )z?Path of the dashboard-client liveness marker under HERMES_HOME.)Úget_hermes_home)Úhermes_constantsrN   r   ÚDASHBOARD_CLIENT_HEARTBEAT_REL)rK   rN   s   & r   Údashboard_client_heartbeat_pathrQ   \   s$   € àÒÝ4Ù%Ó'ˆÜ�ÓÔ=Õ=Ð=r   c               ó    € V ^8„  d   QhRRRR/# )r   ÚpathrL   r   r!   r   )r   s   "r   r   r   d   s   € ÷ 
ñ 
Ð+Fð 
ÐRVñ 
r   c                óJ  €  V f   \        4       MT p\        P                  ! \        P                  P	                  V4      RR7       \        VRRR7      P                  4        \        P                  ! VR4       R#   \         d    \        P                  RRR7        R	# i ; i)
zKMark "a dashboard client is attached right now". Best-effort, never raises.NT)Úexist_okÚaúutf-8)Úencodingz/scale-to-zero: dashboard heartbeat touch failed)Úexc_infoF)rQ   r   ÚmakedirsrS   ÚdirnameÚopenÚcloseÚutimeÚ	ExceptionÚloggerÚdebug)rS   r9   s   & r   Ú touch_dashboard_client_heartbeatrb   d   s{   € ðØ15²Ô+Ô-À4ˆÜ
�Š”B—G‘G—O‘O AÓ&°Õ6ÜˆQ�˜gÔ&×,Ñ,Ô.Ü
�Š��DÔÙøÜô Ü�‰ÐFÐQUˆÔVÚðús   ‚A8A< Á<"B"Â!B"Únowc               ó$   € V ^8„  d   QhRRRRRR/# )r   rS   rL   rc   zOptional[float]r   r   )r   s   "r   r   r   q   s$   € ÷ ñ Ð%@ð Ø$3ðØ?Nñr   c               óî   € Vf   \         P                   ! 4       MTpV f   \        4       MT p \        \        P                  ! V4      P
                  V4      #   \         d     R# \         d    Tu # i ; i)uH  Epoch seconds a dashboard client last sent a WS frame, or None if never. Missing marker ->
None (steady state when nobody has the dashboard open â€” NOT fail-awake, or no instance would
ever sleep). Unreadable marker -> ``now`` (fail-awake, as in ``is_idle``). Clamped to now: an
NTP step-back can leave the mtime in the future.N)ÚtimerQ   Úminr   ÚstatÚst_mtimeÚFileNotFoundErrorÚOSError)rS   rc   Úcurrentr9   s   &$  r   Údashboard_client_last_seenrm   q   sc   € ð !š[Œd�iŠiŒk¨c€GØ-1ª\Ô'Ô)¸t€AðÜ”2—7’7˜1“:×&Ñ&¨Ó0Ð0øÜô ÚÜô ØŠðús   ­)A ÁA4Á%A4Á.A4Á3A4c               ó    € V ^8„  d   QhRRRR/# r    r   )r   s   "r   r   r   �   s   € ÷ 4ñ 4 Nð 4¸dñ 4r   c                óº   € \        \        V \        4      ;'       d<    \        V \        4      ;'       d$    \        P
                  P                  \        4      4      # )zÓTrue iff Fly machine identity is present AND the local Machines API socket exists.
Off-Fly this is False; see ``suspend_available`` for whether some OTHER lever exists
before concluding the watcher must abstain.)r!   r   ÚFLY_APP_NAME_ENVÚFLY_MACHINE_ID_ENVr   rS   ÚexistsÚFLY_API_SOCKETr&   s   &r   Úself_suspend_availablert   �   sI   € ô ”˜Ô"2Ó3÷ 3ð 3¼ÀÔJ\Ó8]÷ 3ð 3Ü—G‘G—N‘N¤>Ó2ó4ð 4r   c               ó    € V ^8„  d   QhRRRR/# )r   r   r   r   r@   r   )r   s   "r   r   r   ‰   s   € ÷ ñ  ð ¸-ñ r   c                óx  € V e   T M\         P                  p\        VP                  \        R4      4      P                  4       pV'       g   R# \        P                  P                  V4      pVP                  R8w  g   VP                  '       g+   \        '       g   Rs\        P                  R\        4       R# V# )a(  The NAS sleep endpoint to POST, or None when this backend has no broker.

Validated here rather than at POST time: a malformed value would otherwise let
the watcher mark draining, hold the re-dial and flip the connector before
urllib rejected it, quiescing for a suspend that could never happen.
Nr   ÚhttpsTzAscale-to-zero: ignoring malformed %s (want an absolute https URL))r   r   r   r   ÚSLEEP_URL_ENVr   ÚurllibÚparseÚurlsplitÚschemeÚnetlocÚ_malformed_sleep_url_loggedr`   Úwarning)r   r   ÚurlÚparseds   &   r   Úbrokered_sleep_urlr‚   ‰   sŠ   € ð Ò(‰'¬b¯j©j€CÜ
ˆc�g‰g”m RÓ(Ó
)×
/Ñ
/Ó
1€CßÙÜ�\‰\×"Ñ" 3Ó'€FØ‡}�}˜Ô v§}§} }÷
 +Ó*Ø*.Ð'Ü�N‰NØSÜôñ Ø€Jr   c               ó    € V ^8„  d   QhRRRR/# r    r   )r   s   "r   r   r   ¤   s   € ÷ Nñ N˜~ð N¸ñ Nr   c                ón   € V e   T M\         P                  p\        V4      ;'       g    \        V4      RJ# )z‰Whether ANY suspend lever exists, in-guest or brokered.

Quiescing without one is worse than not quiescing: the re-dial clears the flip.
N)r   r   rt   r‚   )r   r   s   & r   Úsuspend_availabler…   ¤   s2   € ð
 Ò(‰'¬b¯j©j€CÜ! #Ó&×MÐMÔ*<¸SÓ*AÈÐ*MÐMr   g      D@g      .@g      Ð?ÚtimeoutÚopenerc               ó(   € V ^8„  d   QhRRRRRRRR/# )	r   r€   r   r†   r,   r‡   r   r   r!   r   )r   s   "r   r   r   Å   s2   € ÷ #ñ #Ø	ð#ð ð#ð ð	#ð
 
ñ#r   c          	     óø  € ^ RI p^ RIpVP                  P                  V RRR7      pT;'       g    VP                  P                  p V! WAR7      ;_uu_ 4       p\        \        VR^ 4      ;'       g    ^ 4      pRRR4       ^ÈXu;8*  ;'       d    R
8  Mu p	T	'       d   \        P!                  RT4       T	# \        P                  RT4       T	#   + '       g   i     L^; i  TP                  P                   d7   p\        P                  RTP                  TP                  4        Rp?R# Rp?iTP                  P                  \        \        3 d"   p\        P                  R	T4        Rp?R# Rp?ii ; i)z�POST the NAS sleep URL so NAS stops this machine on our behalf.

Same contract as ``suspend_self``: never raises, True only on 2xx, fail-awake.
Nr   ÚPOST)ÚdataÚmethod)r†   Ústatusz/scale-to-zero: brokered suspend rejected: %s %sFz2scale-to-zero: brokered suspend request failed: %sé,  z3scale-to-zero: machine suspend accepted by NAS (%s)z+scale-to-zero: brokered suspend returned %s)Úurllib.errorÚurllib.requestÚrequestÚRequestÚurlopenr+   r7   ÚerrorÚ	HTTPErrorr`   r   ÚcodeÚreasonÚURLErrorrk   r/   Úinfo)
r€   r†   r‡   ry   r‘   Úopen_urlÚresponser�   ÚexcÚoks
   &$$       r   Úrequest_brokered_suspendrž   Å   s#  € ó Ûð �n‰n×$Ñ$ S¨s¸6Ð$ÓB€GØ×/Ð/˜Ÿ™×/Ñ/€HðÙ�g×/Õ/°8Üœ ¨8°QÓ7×<Ð<¸1Ó=ˆF÷ 0ð 
�×	Ô	˜Ô	€Bß	Ü�‰ÐIÈ6ÔRð €Iô 	�‰ÐDÀfÔMØ€I÷% 0×/ûà�<‰<×!Ñ!ô ä�‰Ø=Ø�H‰HØ�J‰Jô	
õ
 ûØ�L‰L×!Ñ!¤7¬JÐ7ô Ü�‰ÐKÈSÔQÝûðúsH   ÁC# Á!CÁ:C# ÃC 	ÃC# Ã C# Ã#E9Ã>+D/Ä/'E9ÅE9ÅE4Å4E9Úsocket_pathg      $@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   s   "r   r   r   ë   s*   € ÷ !ñ !˜.ð !Àð !Øð!Ø+/ñ!r   c               óÎ  € \        V \        4      \        V \        4      rCV'       d	   V'       g   \        P	                  R4       R# RV RV R2p \
        P
                  ! \
        P                  \
        P                  4      ;_uu_ 4       pVP                  V4       VP                  V4       VP                  VP                  R4      4       Rp\        V4      R8  d$   VP                  R	4      ;p'       d   Wx,          pK3  R
R
R
4       XP                  R^4      ^ ,          P!                  RR4      p
T
P                  4       p\        T4      ^8¬  ;'       dF    T^,          P#                  4       ;'       d'    ^È\%        T^,          4      u;8*  ;'       d    R8  Mu pT'       d   \        P'                  RT
4       T# TP                  R^4      R,          R,          P!                  RR4      p\        P	                  RT
\(        P*                  ! T4      R,          4       T#   + '       g   i     EL); i  \         d"   p	\        P	                  RT	4        R
p	?	R# R
p	?	ii ; i)uŒ  POST /v1/apps/{app}/machines/{id}/suspend on the local flaps socket (the socket is the
credential). Returns True when flaps accepted (2xx); the kernel then freezes this process
shortly after, so treat as fire-and-forget. Never raises: a failed suspend leaves the machine
running (fail-awake). stdlib-only on purpose â€” a plain unix-socket HTTP/1.1 request, no async
plumbing to freeze mid-await.z?scale-to-zero: suspend_self called without Fly machine identityFzPOST /v1/apps/z
/machines/zH/suspend HTTP/1.1
Host: flaps
Content-Length: 0
Connection: close

Úasciir   i   i   Nz/scale-to-zero: flaps suspend request failed: %ss   
ÚreplacerŽ   z5scale-to-zero: machine suspend accepted by flaps (%s)s   

:Niô  NrW   z,scale-to-zero: flaps suspend rejected: %s %séÿÿÿÿ)r   rp   rq   r`   r   ÚsocketÚAF_UNIXÚSOCK_STREAMÚ
settimeoutÚconnectÚsendallÚencodeÚlenÚrecvrk   ÚsplitÚdecodeÚisdigitr+   r™   ÚjsonÚdumps)r   rŸ   r†   ÚappÚ
machine_idr‘   Úsockr›   Úchunkrœ   Ústatus_lineÚpartsr�   Úbodys   &$$           r   Úsuspend_selfrº   ë   sÓ  € ô ˜wÔ(8Ó9¼8ÀGÔM_Ó;`ˆß—jÜ�‰ÐXÔYÙØ ˜u J¨z¨lð ;Oð O€Gð
Ü�]Š]œ6Ÿ>™>¬6×+=Ñ+=×>Ô>À$Ø�O‰O˜GÔ$Ø�L‰L˜Ô%Ø�L‰L˜Ÿ™¨Ó0Ô1ØˆHÜ�h“- %Ô'°d·i±iÀ³oÐ-E¨UÖ-EØÕ!’÷ ?ð —.‘. ¨!Ó,¨QÕ/×6Ñ6°wÀ	ÓJ€KØ×ÑÓ€EÜ	ˆU‹�q‰×	NÐ	N˜U 1�X×-Ñ-Ó/×	NÐ	N°C¼3¸uÀQ½x»=×4NÔ4NÈ3Ô4N€Bß	Ü�‰ÐKÈ[ÔYð
 €Ið �~‰~˜k¨1Ó-¨bÕ1°$Õ7×>Ñ>¸wÈ	ÓRˆÜ�‰ÐEÀ{Ü—z’z $Ó'¨Õ-ô	/à€I÷' ?×>Ð>ûô ô Ü�‰ÐHÈ#ÔNÝûðús=   Á;H8 ÂA-H$Ã:
H$ÄH8 È$H5	È/H8 È5H8 È8I$ÉIÉI$>   Ú1ÚonÚyesÚtrue)N)/Ú__doc__Ú
__future__r   r±   Úloggingr   r¥   rf   Úurllib.parsery   Úpathlibr   Útypingr   r   r   Ú	getLoggerÚ__name__r`   r#   rp   rq   rs   rx   r~   ÚDEFAULT_IDLE_TIMEOUT_MINUTESr%   rS   ÚjoinrP   r   r'   r1   r;   rB   rI   rQ   rb   rm   rt   r‚   r…   ÚBROKERED_SUSPEND_TIMEOUT_SÚFLY_FREEZE_GRACE_SÚFLY_FREEZE_GRACE_TICK_Srž   rº   r   r   r   Ú<module>rÌ      s#  ðñõ #ã Û Û 	Û Û Û Ý ß *Ñ *à	×	Ò	˜8Ó	$€à*Ð Ø!Ð Ø%Ð à€ð *€à#Ð ð  !Ð Ú
$€ð
 "$§¡§¡¨gÐ7TÓ!UÐ õJ÷Cð 7S÷HõõKõD÷>÷
ñØ6:÷ð ÷ 4÷÷6Nð "Ð ð Ð ð Ð ð#ð 0ð#ð ÷	#ñL!À~ð !Ø"&÷!ò !r   