+
    jHàj`4  ã                  ó  € R t ^ RIHt ^ RIt^ RIt^ RIt^ RIt^ RIHtHtH	t	H
t
 ^ RIHtHt ^ RIHt ]'       d	   ^ RIHtHt ]P&                  ! R4      tR R	 ltR
 R ltR R ltR R ltRtR R lt ! R R4      tR# )z^SessionStore explicit suspension, crash-recovery markers, pruning and shared clock/id helpers.)ÚannotationsN)ÚdatetimeÚ	timedeltaÚtimezoneÚUTC)ÚTYPE_CHECKINGÚOptional©Únew_session_id)ÚSessionEntryÚSessionSourcezgateway.sessionc               ó   € V ^8„  d   QhRR/# )é   Úreturnr   © )Úformats   "Ú>/home/ubuntu/.hermes/hermes-agent/gateway/session_lifecycle.pyÚ__annotate__r      s   € ÷ ñ ˆhñ ó    c                 ó,   € \         P                  ! 4       # )zReturn the current local time.)r   Únowr   r   r   Ú_nowr      s   € ä�<Š<‹>Ðr   c               ó    € V ^8„  d   QhRRRR/# )r   r   r   r   Ústrr   )r   s   "r   r   r      s   € ÷ *ñ *˜ð * cñ *r   c                ó   € \        V ^R7      # )é   )Úhex_lenr	   )r   s   &r   Ú_new_session_idr      s   € Ü˜# qÔ)Ð)r   c               ó    € V ^8„  d   QhRRRR/# )r   ÚdtúOptional[datetime]r   úOptional[str]r   )r   s   "r   r   r      s   € ÷ *ñ *Ðð * Mñ *r   c                ó6   € V '       d   V P                  4       # R # ©N)Ú	isoformat)r   s   &r   Ú_isor%      s   € ßˆ2�<‰<‹>Ð) TÐ)r   c               ó   € V ^8„  d   QhRR/# )r   r   r    r   )r   s   "r   r   r   "   s   € ÷ ñ Ð+ñ r   c                ót   € V '       g   R#  \         P                  ! V 4      #   \        \        3 d     R# i ; i)zG``datetime.fromisoformat`` that returns None for empty/malformed input.N)r   ÚfromisoformatÚ	TypeErrorÚ
ValueError)Úvalues   &r   Ú
_parse_isor,   "   s5   € çÙðÜ×%Ò% eÓ,Ð,øÜ”zÐ"ô Úðús   Œ" ¢7¶7c               ó   € V ^8„  d   QhRR/# )r   r   Úfloatr   )r   s   "r   r   r   1   s   € ÷ <ñ <¨ñ <r   c                 óÒ   € \         P                  P                  R4      p  V '       d   \        V 4      # \        \        4      #   \
        \        3 d    \        \        4      u # i ; i)zRResume-scheduler freshness window; stale automation never discards the transcript.ÚHERMES_AUTO_CONTINUE_FRESHNESS)ÚosÚenvironÚgetr.   Ú%_AUTO_CONTINUE_FRESHNESS_SECS_DEFAULTr)   r*   )Úraws    r   Úauto_continue_freshness_windowr6   1   sQ   € ä
�*‰*�.‰.Ð9Ó
:€Cð<ß Œu�S‹zÐR¤eÔ,QÓ&RÐRøÜ”zÐ"ô <ÜÔ:Ó;Ò;ð<ús   ¡A ©
A ´A Á A&Á%A&c                  óÎ   € ] tR t^:tRtR R R lltR R ltR R ltR	 R
 ltR R lt	R R lt
R R ltR R ltR R ltR!R R lltR R ltR"R R lltR R ltR R ltRtR# )#ÚSessionLifecycleMixinz<SessionStore explicit boundaries and crash-recovery markers.Nc               ó$   € V ^8„  d   QhRRRRRR/# )r   Ú
session_idr   Úsession_keyr!   r   Úboolr   )r   s   "r   r   Ú"SessionLifecycleMixin.__annotate__=   s'   € ÷ @ñ @°#ð @ÀMð @Ð]añ @r   c                ó   € V'       d   V P                  V4      MV P                  V4      pV'       d	   V'       g   R#  VP                  V4      pTRJ ;'       g    TP	                  R4      RJ#   \         d     R# i ; i)u  True iff state.db says the session is gone: ended (non-null end_reason) or hard-deleted
(no row in a readable owning DB). No DB or a DB error -> False (same failure mode as
``_prune_stale_sessions_locked``). Lets routing self-heal a session finalized or deleted
while the gateway stays alive. Store resolved from the owning profile.

Used by ``get_or_create_session`` to self-heal at routing time: ``_prune_stale_sessions_locked``
only runs at startup, so a session ended in the DB while the gateway stays alive (any path that
finalizes the row without clearing sessions.json) would otherwise be reused as a live routing key
and silently swallow every subsequent message until the next restart (#54878 â€” the live-gateway
variant of #52804/FM9). A hard delete is the same shape one step further: the row is GONE, not
merely ended, and reusing the route makes run_agent's INSERT OR IGNORE resurrect the deleted
session with its old id (#42422) â€” so a missing row is treated exactly like an ended one. DB
errors are non-fatal â€” never block routing on a failed lookup.
The store is resolved from the row's owning profile rather than the ambient scope: an unscoped
background writer keeps its own copy of the same session, and comparing against that copy reports a
live session as ended (#66887).

Pass *session_key* when the owning key is known but the id may have left the routing index:
after the self-heal re-homes the key, the id has no owner and would resolve to the launch
store, which never holds a routed profile's rows (#118862).
FNÚ
end_reason)Ú_db_for_keyÚ_db_for_session_idÚget_sessionÚ	Exceptionr3   )Úselfr:   r;   ÚdbÚrows   &&&  r   Ú_is_session_ended_in_dbÚ-SessionLifecycleMixin._is_session_ended_in_db=   su   € ÷, /:ˆT×Ñ˜kÔ*¸t×?VÑ?VÐWaÓ?bˆßŸÙð	Ø—.‘. Ó,ˆCð �dˆ{×?Ð?˜cŸg™g lÓ3¸4Ð?Ð?øô ô 	Úð	ús   ¾A. Á.A=Á<A=c               ó    € V ^8„  d   QhRRRR/# )r   ÚentryzOptional[SessionEntry]r   r!   r   )r   s   "r   r   r=   \   s   € ÷ ñ Ð+Að Àmñ r   c                ó  € \        VRR4      pVe+   \        VRR4      e   VP                  P                  R8w  d   R# V P                  VP                  4      pVe   \        VRR4      f   R# ^ RIp^ RIHp ^ RIH	p  V! V\        VP                  4      4      pVP                  R4      VP                  8w  d   R# \        VR	,          R
,          4      ;'       g    R#   YdP                  \        \        \         3 d     R# i ; i)a=  The physical target the authority's receipt names for *entry*'s local route, else None
(not a local route, no receipt, or the receipt belongs to another route).

Such a route moves only through the authority (compression and reset advance the receipt
and the route in one transaction), and its first admitted turn reopens a stamped row
(``reopen_local_session``). A row stamped ended (a viewer's ``ws_orphan_reap``) is
therefore never a stale route: dropping it minted a replacement id the authority refuses
(``admission_conflict``) and stranded the conversation (#140642).ÚoriginNÚplatformÚlocalÚ	_read_ctx)Úlocal_receipt)ÚRuntimeStoreErrorÚrouterJ   r:   )ÚgetattrrM   r+   r@   r;   Úsqlite3Úhermes_state_localrP   Úhermes_state_runtimerQ   r   Úchat_idr3   ÚErrorÚKeyErrorr)   ÚAttributeError)rD   rJ   rL   rE   rT   rP   rQ   Úreceipts   &&      r   Ú_local_receipt_targetÚ+SessionLifecycleMixin._local_receipt_target\   sß   € ô ˜ ¨$Ó/ˆØŠ>œW V¨Z¸Ó>ÒFÈ&Ï/É/×J_ÑJ_ÐcjÔJjÙØ×Ñ˜e×/Ñ/Ó0ˆØŠ:œ  [°$Ó7Ò?ÙÛå4Ý:ð	Ù# B¬¨F¯N©NÓ(;Ó<ˆGØ�{‰{˜7Ó# u×'8Ñ'8Ô8ÙÜ�w˜wÕ'¨Õ5Ó6×>Ð>¸$Ð>øØ!§=¡=´(¼IÄ~ÐVô 	Úð	ús   Á?<C  Â=C  ÃC  Ã !DÄDc               ó    € V ^8„  d   QhRRRR/# )r   rJ   r   r   r!   r   )r   s   "r   r   r=   w   s   € ÷ 8ñ 8¨ð 8¸-ñ 8r   c                ó.   € VP                   '       d   R# R# )zIOnly explicit suspension replaces a routed conversation; time never does.Ú	suspendedN)r`   )rD   rJ   s   &&r   Ú_route_reset_reasonÚ)SessionLifecycleMixin._route_reset_reasonw   s   € à#ŸoŸo˜oˆ{Ð7°4Ð7r   c               ó    € V ^8„  d   QhRRRR/# ©r   r;   r   r   r<   r   )r   s   "r   r   r=   {   s   € ÷ ñ ¨ð ¸ñ r   c                óâ   € V P                   ;_uu_ 4        V P                  V4      pVe   V! V4      RJ d    RRR4       R# V P                  4         RRR4       R#   + '       g   i     R# ; i)z‡Apply ``mutate(entry)`` under ``_lock`` and full-save; False when the entry is missing
or *mutate* returned False (nothing to persist).NFT)Ú_lockÚ_entry_lockedÚ_save)rD   r;   ÚmutaterJ   s   &&& r   Ú_update_entryÚ#SessionLifecycleMixin._update_entry{   sQ   € ð �Z�Z‹ZØ×&Ñ& {Ó3ˆEØŠ}¡ u£°Ó 6Ø÷ ‰Zð �J‰JŒLØ÷ �Z�ZŠZús   •#AÁAÁA.	c               ó   € V ^8„  d   QhRR/# ©r   r   Úintr   )r   s   "r   r   r=   …   s   € ÷ ñ °Cñ r   c                ó  a€ V P                   ;_uu_ 4        V P                  4        \        V3R lV P                  P	                  4        4       4      pV'       d   V P                  4        RRR4       V#   + '       g   i     X# ; i)z{Apply ``mutate(entry) -> bool`` to every entry under ``_lock``; save once if any
returned True. Returns the count that did.c              3  óH   <"  € T F  pS! V4      '       g   K  ^x € K  	  R# 5i)é   Nr   )Ú.0rJ   ri   s   & €r   Ú	<genexpr>ÚCSessionLifecycleMixin._update_all_entries_locked.<locals>.<genexpr>Š   s   øé € ÐQÑ)? Á6È%Ç=Ÿ!š!Ó)?ùs   ƒ"˜
"N)rf   Ú_ensure_loaded_lockedÚsumÚ_entriesÚvaluesrh   )rD   ri   Úchangeds   &f r   Ú_update_all_entries_lockedÚ0SessionLifecycleMixin._update_all_entries_locked…   s^   ø€ ð �Z�Z‹ZØ×&Ñ&Ô(ÜÔQ¨¯©×)=Ñ)=Ô)?ÓQÓQˆGßØ—
‘
”÷	 ð
 ˆ÷ ŽZð
 ˆús   –AA6Á6B	c               ó    € V ^8„  d   QhRRRR/# rd   r   )r   s   "r   r   r=   �   s   € ÷ Xñ X¨3ð X°4ñ Xr   c                ó(   € V P                  VR 4      # )z¸Mark a session suspended so it auto-resets on next access (/stop). True if it existed.

Used by ``/stop`` to prevent stuck sessions from being resumed after a gateway restart (#7536).
c                ó   € \        V R R4      # )r`   T)Úsetattr)Úes   &r   Ú<lambda>Ú7SessionLifecycleMixin.suspend_session.<locals>.<lambda>”   s   € ¼ÀÀKÐQUÔ9Vr   ©rj   )rD   r;   s   &&r   Úsuspend_sessionÚ%SessionLifecycleMixin.suspend_session�   s   € ð
 ×!Ñ! +Ñ/VÓWÐWr   c               ó$   € V ^8„  d   QhRRRRRR/# )r   r;   r   rJ   r   r   ÚNoner   )r   s   "r   r   r=   –   s"   € ÷ 'ñ '°3ð '¸|ð 'Ðcgñ 'r   c                óî   € VP                  4       pW5R&   \        V4      VR&   Ve   \        4       MRpVe   VP                  4       VR&   V P	                  WRR7       W2n        WBn        Ve	   Wbn        R# R# )z�Persist the active-turn pair BEFORE publishing it in memory, so a failed write can
neither leak an unowned token nor drop a live one. Lock held.Úactive_turn_tokenÚactive_turn_started_atNÚ
updated_atT)Ú
entry_dataÚ	lock_held)Úto_dictr%   r   r$   Ú_save_entryr‰   rŠ   r‹   )rD   r;   rJ   ÚtokenÚ
started_atÚ	candidateÚtoucheds   &&&&&  r   Ú_set_turn_marker_lockedÚ-SessionLifecycleMixin._set_turn_marker_locked–   s�   € ð —M‘M“Oˆ	Ø).Ð%Ñ&Ü.2°:Ó.>ˆ	Ð*Ñ+Ø&Ò2”$”&¸ˆØÒð '.×&7Ñ&7Ó&9ˆI�lÑ#Ø×Ñ˜ÀdÐÔKØ"'ÔØ'1Ô$ØÒØ&Öñ r   c               ó    € V ^8„  d   QhRRRR/# )r   r;   r   r   r!   r   )r   s   "r   r   r=   §   s   € ÷ ñ ¨Cð °Mñ r   c           
     ó<  € \         P                  ! 4       P                  pV P                  ;_uu_ 4        V P	                  V4      pVf    RRR4       R# V P                  WV\        P                  ! \        4      4       RRR4       V#   + '       g   i     T# ; i)zËPersist exact ownership of the running agent turn; returns the opaque token for
:meth:`clear_turn_active`. Re-marking replaces the previous token so a stale asynchronous
unwind cannot clear a newer turn.N)	ÚuuidÚuuid4Úhexrf   rg   r”   r   r   r   ©rD   r;   r�   rJ   s   &&  r   Úmark_turn_activeÚ&SessionLifecycleMixin.mark_turn_active§   sv   € ô —
’
“× Ñ ˆØ�Z�Z‹ZØ×&Ñ& {Ó3ˆEØŠ}Ø÷ ‰Zð ×(Ñ(¨¸UÄHÇLÂLÔQTÓDUÔV÷ ð ˆ÷ ŽZð ˆús   ´B
Á+B
Â
B	c               ó$   € V ^8„  d   QhRRRRRR/# )r   r;   r   r�   r   r<   r   )r   s   "r   r   r=   µ   s!   € ÷ ñ ¨Sð ¸ð Àñ r   c                óð   € V P                   ;_uu_ 4        V P                  V4      pVe   VP                  V8w  d    RRR4       R# V P                  WRR4       RRR4       R#   + '       g   i     R# ; i)zkCompare-and-swap clear an active-turn marker; ``False`` when the entry disappeared or a
newer turn owns it.NFT)rf   rg   r‰   r”   r›   s   &&& r   Úclear_turn_activeÚ'SessionLifecycleMixin.clear_turn_activeµ   sd   € ð �Z�Z‹ZØ×&Ñ& {Ó3ˆEØŠ} × 7Ñ 7¸5Ô @Ø÷ ‰Zð ×(Ñ(¨¸TÀ4ÔH÷	 ñ
 ÷ ŽZñ
 ús   •(A$ÁA$Á$A5	c               ó    € V ^8„  d   QhRRRR/# )r   Úmax_age_secondsrn   r   r   )r   s   "r   r   r=   ¿   s   € ÷ ñ ¸ð È3ñ r   c                óŒ   aaaa€ \        4       \        P                  ! 4       uoo^ oR VVVV3R llpV P                  V4       S# )z×Promote crash-left turn markers into ``resume_pending`` (unclean startup only).
Old/invalid markers are cleared without resuming; suspended sessions are never re-armed.
Returns the number of newly promoted sessions.c               ó    € V ^8„  d   QhRRRR/# ©r   rJ   r   r   r<   r   )r   s   "r   r   ÚESessionLifecycleMixin.recover_interrupted_turns.<locals>.__annotate__Æ   s   € ÷ 	ñ 	˜Lð 	¨Tñ 	r   c                óŠ  <€ V P                   '       g   R # V P                  pVRJ ;'       g(    S^ 8„  ;'       d    SVP                  4       ,
          S8„  pV'       gY   V P                  '       gG   V P                  '       d   V P
                  f   SV n        MRV n        RV n        SV n        S^,          oRV n         RV n        R# )FNTÚrestart_interrupted)r‰   rŠ   Ú	timestampr`   Úresume_pendingÚlast_resume_marked_atÚresume_reason)rJ   r‘   Úmarker_is_staleÚ	epoch_nowr£   r   Úpromoteds   &  €€€€r   Ú_promoteÚASessionLifecycleMixin.recover_interrupted_turns.<locals>._promoteÆ   s¹   ø€ à×*×*Ð*ÙØ×5Ñ5ˆJà(¨DÐ0÷ ð Ø !Ñ#×\Ð\¨	°J×4HÑ4HÓ4JÕ(JÈ_Ñ(\ð ÷ #¨5¯?¯?¨?Ø×'×'Ð'à×2Ñ2Ò:Ø69˜Ô3øà+/�EÔ(Ø*?�EÔ'Ø25�EÔ/Ø •M�HØ&*ˆEÔ#Ø+/ˆEÔ(Ùr   )r   Útimerz   )rD   r£   r±   r¯   r   r°   s   &f @@@r   Úrecover_interrupted_turnsÚ/SessionLifecycleMixin.recover_interrupted_turns¿   s<   û€ ô ›¤§¢£ˆˆˆYØˆ÷	ó 	ð. 	×'Ñ'¨Ô1Øˆr   c               ó   € V ^8„  d   QhRR/# rm   r   )r   s   "r   r   r=   à   s   € ÷ 9ñ 9¨Sñ 9r   c                ó0   € R R lpV P                  V4      # )z:Clear orphan turn markers after a verified clean shutdown.c               ó    € V ^8„  d   QhRRRR/# r¦   r   )r   s   "r   r   ÚGSessionLifecycleMixin.discard_active_turn_markers.<locals>.__annotate__â   s   € ÷ 	ñ 	˜Lð 	¨Tñ 	r   c                óf   € V P                   '       g   V P                  f   R# R V n         R V n        R# )NFT)r‰   rŠ   ©rJ   s   &r   Ú_discardÚCSessionLifecycleMixin.discard_active_turn_markers.<locals>._discardâ   s/   € Ø×*×*Ð*¨u×/KÑ/KÒ/SÙØ&*ˆEÔ#Ø+/ˆEÔ(Ùr   )rz   )rD   r¼   s   & r   Údiscard_active_turn_markersÚ1SessionLifecycleMixin.discard_active_turn_markersà   s   € õ	ð ×.Ñ.¨xÓ8Ð8r   c               ó$   € V ^8„  d   QhRRRRRR/# )r   r;   r   Úreasonr   r<   r   )r   s   "r   r   r=   ê   s"   € ÷ 	7ñ 	7¨sð 	7¸Cð 	7ÐX\ñ 	7r   c                ó8   a€ R V3R llpV P                  W4      # )z„Mark a session resumable after a restart interruption (keeps the session_id/transcript,
unlike ``suspend_session``). True if marked.c               ó   € V ^8„  d   QhRR/# ©r   rJ   r   r   )r   s   "r   r   Ú?SessionLifecycleMixin.mark_resume_pending.<locals>.__annotate__í   s   € ÷ 	1ñ 	1˜,ñ 	1r   c                ój   <€ V P                   '       d   R # RV n        SV n        \        4       V n        R# )FTN)r`   r«   r­   r   r¬   )rJ   rÁ   s   &€r   Ú_applyÚ9SessionLifecycleMixin.mark_resume_pending.<locals>._applyí   s)   ø€ Ø��ˆÙØ#'ˆEÔ Ø"(ˆEÔÜ*.«&ˆEÖ'r   rƒ   )rD   r;   rÁ   rÇ   s   &&f r   Úmark_resume_pendingÚ)SessionLifecycleMixin.mark_resume_pendingê   s    ø€ ÷	1ð 	1ð ×!Ñ! +Ó6Ð6r   c               ó    € V ^8„  d   QhRRRR/# rd   r   )r   s   "r   r   r=   õ   s   € ÷ 7ñ 7°ð 7¸ñ 7r   c                ó0   € R R lpV P                  W4      # )zOClear the resume-pending flag after a successful resumed turn; True if cleared.c               ó   € V ^8„  d   QhRR/# rÄ   r   )r   s   "r   r   Ú@SessionLifecycleMixin.clear_resume_pending.<locals>.__annotate__÷   s   € ÷ 	/ñ 	/˜,ñ 	/r   c                óX   € V P                   '       g   R # R V n         RV n        RV n        R# )FN)r«   r­   r¬   r»   s   &r   rÇ   Ú:SessionLifecycleMixin.clear_resume_pending.<locals>._apply÷   s)   € Ø×'×'Ð'ÙØ#(ˆEÔ Ø"&ˆEÔØ*.ˆEÖ'r   rƒ   )rD   r;   rÇ   s   && r   Úclear_resume_pendingÚ*SessionLifecycleMixin.clear_resume_pendingõ   s   € õ	/ð ×!Ñ! +Ó6Ð6r   c               ó    € V ^8„  d   QhRRRR/# )r   Úmax_age_daysrn   r   r   )r   s   "r   r   r=   ÿ   s   € ÷ !ñ !¨cð !°cñ !r   c                óÂ  € Ve   V^ 8:  d   ^ # \        4       \        VR7      ,
          pV P                  ;_uu_ 4        V P                  4        \	        V P
                  P                  4       4       UUu. uFS  w  r4VP                  '       d   K  V P                  VP                  RR7      '       d   K>  VP                  V8  g   KQ  VNKU  	  pppV F  pV P
                  P                  VR4       K!  	  V'       d   V P                  4        RRR4       X'       d!   \        P                  R\        V4      V4       \        V4      # u uppi   + '       g   i     LI; i)a  Drop routing entries idle (by ``updated_at``) for more than max_age_days; suspended
entries and entries with active background processes are kept. Only the key -> session_id
mapping is dropped (the transcript stays). ``max_age_days <= 0`` disables. Returns count.N)ÚdaysÚprune)Úcontextz1SessionStore pruned %d entries older than %d days)r   r   rf   ru   Úlistrw   Úitemsr`   Ú_has_active_processes_safer;   r‹   Úpoprh   ÚloggerÚinfoÚlen)rD   rÔ   ÚcutoffÚkeyrJ   Úremoved_keyss   &&    r   Úprune_old_entriesÚ'SessionLifecycleMixin.prune_old_entriesÿ   s  € ð Ò <°1Ô#4ÙÜ“œ)¨Ô6Õ6ˆØ�Z�Z‹ZØ×&Ñ&Ô(ä&*¨4¯=©=×+>Ñ+>Ó+@Ô&AôÙ&A™
˜Ø—•ô ð ×7Ñ7¸×8IÑ8IÐSZÐ7×[ô ð ×$Ñ$ vÑ-÷	 �Ñ&Að ñ ó $�Ø—‘×!Ñ! # tÖ,ñ $çØ—
‘
”÷ ÷ Ü�K‰KÐKÜ˜LÓ)¨<ô9ä�<Ó Ð ùó÷ �Zús5   ½7EÁ4E
Â E
Â4E
ÃE
Ã/EÃ=EÅEÅE	r   r#   )é  )Úrestart_timeout)Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__Ú__doc__rG   r\   ra   rj   rz   r„   r”   rœ   r    r´   r¾   rÉ   rÑ   rã   Ú__static_attributes__r   r   r   r8   r8   :   sS   † ÙF÷@õ>õ68õõõXõ'õ"õ÷õB9÷	7õ7÷!ñ !r   r8   rå   )rë   Ú
__future__r   Úloggingr1   r³   r˜   r   r   r   r   Útypingr   r   Úhermes_state_idsr
   Úgateway.sessionr   r   Ú	getLoggerrÝ   r   r   r%   r,   r4   r6   r8   r   r   r   Ú<module>ró      si   ðÙ då "ã Û 	Û Û ß 7Ó 7ß *å +çß;ð 
×	Ò	Ð,Ó	-€õõ
*õ*õð )0Ð %õ<÷\!ó \!r   