Ë
    âß²jh/  ã                  óÐ   — d Z ddlmZ ddlZddlZddlZddlmZmZ ddlm	Z	m
Z
 ddlmZ e	rddlmZmZ  ej                   d«      Zdd	„Zdd
„Zdd„Zdd„ZdZdd„Z G d„ d«      Zy)z^SessionStore explicit suspension, crash-recovery markers, pruning and shared clock/id helpers.é    )ÚannotationsN)ÚdatetimeÚ	timedelta)ÚTYPE_CHECKINGÚOptional©Únew_session_id)ÚSessionEntryÚSessionSourcezgateway.sessionc                 ó*   — t        j                  «       S )zReturn the current local time.)r   Únow© ó    ú>/home/ubuntu/.hermes/hermes-agent/gateway/session_lifecycle.pyÚ_nowr      s   € ä�<‰<‹>Ðr   c                ó   — t        | d¬«      S )Né   )Úhex_lenr   )r   s    r   Ú_new_session_idr      s   € Ü˜# qÔ)Ð)r   c                ó*   — | r| j                  «       S d S )N)Ú	isoformat)Údts    r   Ú_isor      s   € Ùˆ2�<‰<‹>Ð) TÐ)r   c                ó^   — | sy	 t        j                  | «      S # t        t        f$ r Y yw xY w)zG``datetime.fromisoformat`` that returns None for empty/malformed input.N)r   ÚfromisoformatÚ	TypeErrorÚ
ValueError)Úvalues    r   Ú
_parse_isor   !   s5   € áØðÜ×%Ñ% eÓ,Ð,øÜ”zÐ"ò Ùðús   … š,«,é  c                 óÂ   — t         j                  j                  d«      } 	 | rt        | «      S t        t        «      S # t
        t        f$ r t        t        «      cY S w xY w)zRResume-scheduler freshness window; stale automation never discards the transcript.ÚHERMES_AUTO_CONTINUE_FRESHNESS)ÚosÚenvironÚgetÚfloatÚ%_AUTO_CONTINUE_FRESHNESS_SECS_DEFAULTr   r   )Úraws    r   Úauto_continue_freshness_windowr)   0   sQ   € ä
�*‰*�.‰.Ð9Ó
:€Cð<Ù Œu�S‹zÐR¤eÔ,QÓ&RÐRøÜ”zÐ"ò <ÜÔ:Ó;Ò;ð<ús   ¡= ®= ½AÁAc                  ó†   — e Zd ZdZdd„Zdd„Zdd„Zdd„Zdd„Zdd„Z	dd„Z
dd	„Zddd
„Zdd„Zddd„Zdd„Zdd„Zddd„Zy)ÚSessionLifecycleMixinz<SessionStore explicit boundaries and crash-recovery markers.c                ó´   — | j                  |«      }|r|sy	 |j                  |«      }t        |duxr |j	                  d«      du«      S # t        $ r Y yw xY w)u$  True iff state.db has this session with a non-null end_reason (same staleness test as
        ``_prune_stale_sessions_locked``; no DB/row or DB error -> False). Lets routing self-heal a
        session ended 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). 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).
        FNÚ
end_reason)Ú_db_for_session_idÚget_sessionÚ	ExceptionÚboolr%   )ÚselfÚ
session_idÚdbÚrows       r   Ú_is_session_ended_in_dbz-SessionLifecycleMixin._is_session_ended_in_db<   sg   € ð ×$Ñ$ ZÓ0ˆÙ™Øð	Ø—.‘. Ó,ˆCô �C˜t�OÒI¨¯©°Ó(=ÀTÐ(IÓJÐJøô ò 	Ùð	ús   ˜A Á	AÁAc                ó"   — |j                   rdS dS )zIOnly explicit suspension replaces a routed conversation; time never does.Ú	suspendedN)r8   )r2   Úentrys     r   Ú_route_reset_reasonz)SessionLifecycleMixin._route_reset_reasonS   s   € à#Ÿošoˆ{Ð7°4Ð7r   c                ó¶   — | j                   5  | j                  |«      }|�
 ||«      du r
	 ddd«       y| j                  «        	 ddd«       y# 1 sw Y   yxY w)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)r2   Úsession_keyÚmutater9   s       r   Ú_update_entryz#SessionLifecycleMixin._update_entryW   s[   € ð �Z‰Zñ 	Ø×&Ñ& {Ó3ˆEØˆ}¡ u£°Ñ 6Ø÷	ð 	ð �J‰JŒLØ÷	÷ 	ñ 	ús   �AµAÁAc                óè   ‡— | j                   5  | j                  «        t        ˆfd„| j                  j	                  «       D «       «      }|r| j                  «        ddd«       |S # 1 sw Y   S xY w)zƒApply ``mutate(entry) -> bool`` to every entry under ``_lock``; save once if any
        returned True. Returns the count that did.c              3  ó4   •K  — | ]  } ‰|«      sŒd –— Œ y­w)é   Nr   )Ú.0r9   r@   s     €r   ú	<genexpr>zCSessionLifecycleMixin._update_all_entries_locked.<locals>.<genexpr>f   s   øè ø€ ÒQ Á6È%Å=œ!ÑQùs   ƒ‘N)r<   Ú_ensure_loaded_lockedÚsumÚ_entriesÚvaluesr>   )r2   r@   Úchangeds    ` r   Ú_update_all_entries_lockedz0SessionLifecycleMixin._update_all_entries_lockeda   s_   ø€ ð �Z‰Zñ 	Ø×&Ñ&Ô(ÜÓQ¨¯©×)=Ñ)=Ó)?ÔQÓQˆGÙØ—
‘
”÷		ð
 ˆ÷	ð
 ˆús   ŽAA'Á'A1c                ó(   — | j                  |d„ «      S )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                ó   — t        | dd«      S )Nr8   T)Úsetattr)Úes    r   ú<lambda>z7SessionLifecycleMixin.suspend_session.<locals>.<lambda>p   s   € ¼ÀÀKÐQUÓ9V€ r   ©rA   )r2   r?   s     r   Úsuspend_sessionz%SessionLifecycleMixin.suspend_sessionk   s   € ð
 ×!Ñ! +Ñ/VÓWÐWr   c                óÌ   — |j                  «       }||d<   t        |«      |d<   |�|j                  «       |d<   | j                  ||d¬«       ||_        ||_        |�||_        yy)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   Ú_save_entryrU   rV   rW   )r2   r?   r9   ÚtokenÚ
started_atÚ	candidates         r   Ú_set_turn_marker_lockedz-SessionLifecycleMixin._set_turn_marker_lockedr   s}   € ð —M‘M“Oˆ	Ø).ˆ	Ð%Ñ&Ü.2°:Ó.>ˆ	Ð*Ñ+ØÐ!ð '1×&:Ñ&:Ó&<ˆI�lÑ#Ø×Ñ˜°ÀdÐÔKØ"'ˆÔØ'1ˆÔ$ØÐ!Ø)ˆEÕð "r   c                óø   — t        j                  «       j                  }| j                  5  | j	                  |«      }|€
	 ddd«       y| j                  |||t        «       «       ddd«       |S # 1 sw Y   |S xY w)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Úhexr<   r=   r_   r   ©r2   r?   r\   r9   s       r   Úmark_turn_activez&SessionLifecycleMixin.mark_turn_active‚   sy   € ô —
‘
“× Ñ ˆØ�Z‰Zñ 	LØ×&Ñ& {Ó3ˆEØˆ}Ø÷	Lð 	Lð ×(Ñ(¨°e¸UÄDÃFÔK÷		Lð
 ˆ÷	Lð
 ˆús   «A/Á	A/Á/A9c                óÆ   — | j                   5  | j                  |«      }|�|j                  |k7  r
	 ddd«       y| j                  ||dd«       ddd«       y# 1 sw Y   yxY w)zsCompare-and-swap clear an active-turn marker; ``False`` when the entry disappeared or a
        newer turn owns it.NFT)r<   r=   rU   r_   rd   s       r   Úclear_turn_activez'SessionLifecycleMixin.clear_turn_activeŽ   so   € ð �Z‰Zñ 	IØ×&Ñ& {Ó3ˆEØˆ} × 7Ñ 7¸5Ò @Ø÷	Ið 	Ið ×(Ñ(¨°e¸TÀ4ÔH÷		Ið
 ÷	Ið
 ús   �$AºAÁA c                ó†   ‡‡‡‡— t        «       Št        t        d‰«      ¬«      ŠdŠdˆˆˆˆfd„}| j                  |«       ‰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.r   ©Úsecondsc                ó2  •— | j                   sy| j                  }	 |d u xs ‰dkD  xr ‰|z
  ‰kD  }|sF| j                  s:| j                  r| j
                  €"‰| _        nd| _        d| _        ‰| _        ‰dz  Šd | _         d | _        y# t        $ r d}Y Œdw xY w)NFr   TÚrestart_interruptedrD   )rU   rV   r   r8   Úresume_pendingÚlast_resume_marked_atÚresume_reason)r9   r]   Úmarker_is_staleÚmax_ageÚmax_age_secondsr   Úpromoteds      €€€€r   Ú_promotezASessionLifecycleMixin.recover_interrupted_turns.<locals>._promote    sº   ø€ à×*Ò*ØØ×5Ñ5ˆJð'Ø",°Ð"4ò #Ø# aÑ'ÒF¨C°*Ñ,<¸wÑ,Fð  ñ #¨5¯?ª?Ø×'Ò'à×2Ñ2Ð:Ø69˜Õ3à+/�EÔ(Ø*?�EÔ'Ø25�EÔ/Ø ‘M�HØ&*ˆEÔ#Ø+/ˆEÔ(Øøô ò 'à"&’ð'ús   œB ÂBÂB©r9   r
   Úreturnr1   )r   r   ÚmaxrL   )r2   rr   rt   rq   r   rs   s    ` @@@r   Úrecover_interrupted_turnsz/SessionLifecycleMixin.recover_interrupted_turns˜   sA   û€ ô ‹fˆÜ¤C¨¨?Ó$;Ô<ˆØˆ÷	ð 	ð4 	×'Ñ'¨Ô1Øˆr   c                ó,   — dd„}| j                  |«      S )z:Clear orphan turn markers after a verified clean shutdown.c                óR   — | j                   s| j                  €yd | _         d | _        y©NFT)rU   rV   ©r9   s    r   Ú_discardzCSessionLifecycleMixin.discard_active_turn_markers.<locals>._discard¿   s,   € Ø×*Ò*¨u×/KÑ/KÐ/SØØ&*ˆEÔ#Ø+/ˆEÔ(Ør   ru   )rL   )r2   r}   s     r   Údiscard_active_turn_markersz1SessionLifecycleMixin.discard_active_turn_markers½   s   € ó	ð ×.Ñ.¨xÓ8Ð8r   c                ó4   ‡— dˆfd„}| j                  ||«      S )zŒMark a session resumable after a restart interruption (keeps the session_id/transcript,
        unlike ``suspend_session``). True if marked.c                óZ   •— | j                   ryd| _        ‰| _        t        «       | _        y r{   )r8   rm   ro   r   rn   )r9   Úreasons    €r   Ú_applyz9SessionLifecycleMixin.mark_resume_pending.<locals>._applyÊ   s'   ø€ Ø�ŠØØ#'ˆEÔ Ø"(ˆEÔÜ*.«&ˆEÕ'r   ©r9   r
   rR   )r2   r?   r�   r‚   s     ` r   Úmark_resume_pendingz)SessionLifecycleMixin.mark_resume_pendingÇ   s   ø€ õ	1ð ×!Ñ! +¨vÓ6Ð6r   c                ó.   — dd„}| j                  ||«      S )zOClear the resume-pending flag after a successful resumed turn; True if cleared.c                óH   — | j                   syd| _         d | _        d | _        y )NF)rm   ro   rn   r|   s    r   r‚   z:SessionLifecycleMixin.clear_resume_pending.<locals>._applyÔ   s&   € Ø×'Ò'ØØ#(ˆEÔ Ø"&ˆEÔØ*.ˆEÕ'r   rƒ   rR   )r2   r?   r‚   s      r   Úclear_resume_pendingz*SessionLifecycleMixin.clear_resume_pendingÒ   s   € ó	/ð ×!Ñ! +¨vÓ6Ð6r   c           	     óL  — |�|dk  ryt        «       t        |¬«      z
  }| j                  5  | j                  «        t	        | j
                  j                  «       «      D ��cg c]?  \  }}|j                  s.| j                  |j                  d¬«      s|j                  |k  r|‘ŒA }}}|D ]  }| j
                  j                  |d«       Œ  |r| j                  «        ddd«       r t        j                  dt        |«      |«       t        |«      S c c}}w # 1 sw Y   Œ<xY w)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.Nr   )ÚdaysÚprune)Úcontextz1SessionStore pruned %d entries older than %d days)r   r   r<   rG   ÚlistrI   Úitemsr8   Ú_has_active_processes_safer?   rW   Úpopr>   ÚloggerÚinfoÚlen)r2   Úmax_age_daysÚcutoffÚkeyr9   Úremoved_keyss         r   Úprune_old_entriesz'SessionLifecycleMixin.prune_old_entriesÜ   s  € ð Ð <°1Ò#4ØÜ“œ)¨Ô6Ñ6ˆØ�Z‰Zñ 	Ø×&Ñ&Ô(ä&*¨4¯=©=×+>Ñ+>Ó+@Ó&A÷Ù"˜˜UØ—’à×7Ñ7¸×8IÑ8IÐSZÐ7Ô[Ø×$Ñ$ vÒ-ò	 ðˆLñ ð $ò -�Ø—‘×!Ñ! # tÕ,ð-áØ—
‘
”÷	ñ Ü�K‰KÐKÜ˜LÓ)¨<ô9ä�<Ó Ð ùó÷	ð 	ús   ¬7DÁ#ADÂ'8DÄDÄD#c                ó`   ‡— t        «       t        |¬«      z
  Šdˆfd„}| j                  |«      S )aI  Mark sessions active within *max_age_seconds* as ``resume_pending`` after a crash/fast
        restart (already-pending and suspended entries are skipped). Returns the number marked.

        Called on gateway startup after a crash or fast restart to preserve in-flight sessions instead of
        destroying their conversation history (#7536). Only marks sessions updated within *max_age_seconds*
        to avoid touching long-idle sessions. Sets ``resume_pending=True`` so the next incoming message on
        the same session_key auto-resumes from the existing transcript.
        ri   c                ó�   •— | j                   s| j                  s| j                  ‰k  ryd| _         d| _        t	        «       | _        y)NFTrl   )rm   r8   rW   ro   r   rn   )r9   r”   s    €r   Ú_markz<SessionLifecycleMixin.suspend_recently_active.<locals>._mark   s?   ø€ Ø×#Ò# u§¢¸%×:JÑ:JÈVÒ:SØØ#'ˆEÔ Ø"7ˆEÔÜ*.«&ˆEÔ'Ør   ru   )r   r   rL   )r2   rr   rš   r”   s      @r   Úsuspend_recently_activez-SessionLifecycleMixin.suspend_recently_activeõ   s.   ø€ ô “œ)¨OÔ<Ñ<ˆõ	ð ×.Ñ.¨uÓ5Ð5r   N)r3   Ústrrv   r1   )r9   r
   rv   úOptional[str])r?   rœ   rv   r1   )rv   Úint)r?   rœ   r9   r
   rv   ÚNone)r?   rœ   rv   r�   )r?   rœ   r\   rœ   rv   r1   )r    )rr   rž   rv   rž   )Úrestart_timeout)r?   rœ   r�   rœ   rv   r1   )r“   rž   rv   rž   )éx   )Ú__name__Ú
__module__Ú__qualname__Ú__doc__r6   r:   rA   rL   rS   r_   re   rg   rx   r~   r„   r‡   r—   r›   r   r   r   r+   r+   9   sN   „ ÙFóKó.8óóóXó*ó 
óô#óJ9ô	7ó7ó!õ26r   r+   )rv   r   )r   r   rv   rœ   )r   úOptional[datetime]rv   r�   )rv   r¦   )rv   r&   )r¥   Ú
__future__r   Úloggingr#   ra   r   r   Útypingr   r   Úhermes_state_idsr	   Úgateway.sessionr
   r   Ú	getLoggerr�   r   r   r   r   r'   r)   r+   r   r   r   ú<module>r­      se   ðÙ då "ã Û 	Û ß (ß *å +áß;ð 
ˆ×	Ñ	Ð,Ó	-€óó
*ó*óð )0Ð %ó<÷N6ò N6r   