
    Pmj4                       d Z ddlmZ ddlZddlZddl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 ddlmZ  ej        e          Zd?d@dZdAdZdBdZdCdZ	 dDdEdZdFdZdFdZdFd Z G d! d"          ZdGd$ZdHd&ZdId)ZdJd*Z dDdKd-Z!dLd.Z"e	 G d/ d0                      Z#dd1dMd7Z$dNd9Z%dd1dOd:Z&dPd=Z'dQd>Z(dS )RzCross-process active chat session leases.

The session database records persisted conversations.  This module records
currently open chat surfaces, including idle CLI/TUI sessions that have not
written a transcript row yet.
    )annotationsN)	dataclass)Path)AnyOptional)get_hermes_homemax_concurrent_sessionsvaluer   keystrreturnOptional[int]c                   | dS t          | t                    rt                              d||            dS 	 t          | t                    r3|                                 st          |           t          |           }nGt          | t                    r#t          | 	                                d          }nt          |           }n4# t          t          f$ r  t                              d||            Y dS w xY w|dk    rdS |S )z=Return a positive integer cap, or None when disabled/invalid.NzEIgnoring invalid %s=%r (expected a positive integer; 0/null disables)
   r   )
isinstanceboolloggerwarningfloat
is_integer
ValueErrorintr   strip	TypeError)r
   r   parseds      @/home/thesage/.hermes/hermes-agent/hermes_cli/active_sessions.pycoerce_max_concurrent_sessionsr      s   }t% S	
 	
 	

 teU## 	 ##%% ( '''ZZFFs## 	 ++FFZZFz"   S	
 	
 	

 tt {{tMs   BC	 	-C:9C:configc                0   d}d}t          | t                    r\d| v r|                     d          }nS|                     d          }t          |t                    r|                    d          }d}nt          | dd          }t	          ||          S )zBResolve top-level max_concurrent_sessions with gateway.* fallback.Nr	   gatewayzgateway.max_concurrent_sessionsr   )r   dictgetgetattrr   )r   rawr   gateway_cfgs       r   resolve_max_concurrent_sessionsr'   8   s    C
#C&$ 	?$..**677CC **Y//K+t,, 8!oo&?@@7f7>>)#37777    secondsr   c                    t          dt          | dz                      }|dk     r| dS t          |d          \  }}|s| dn| d| dS )Nr   <   mh)maxr   divmod)r)   minuteshourss      r   
format_ager2   I   sk    !SB''((G||}}}GR((NE7%@e;;;;e+@+@g+@+@+@@r(   entrieslist[dict[str, Any]]c                   | sdS i }| D ]B}t          |                    d          pd          }|                    |d          dz   ||<   Cd                    d t          |                                d 	          D                       }d
 d | D             D             }|r7|dt          t          j                    t          |          z
             dz  }|S )zDCompact "who is holding the slots" phrase, e.g. ``desktop x4, cli``. surfaceunknownr      z, c              3  :   K   | ]\  }}|d k    r| d| n|V  dS )r9   z xN ).0r7   ns      r   	<genexpr>z$summarize_holders.<locals>.<genexpr>Y   sU        GQ UU7a     r(   c                $    | d          | d         fS )Nr9   r   r;   )kvs    r   <lambda>z#summarize_holders.<locals>.<lambda>[   s    "Q%A r(   r!   c                    g | ]}||S r;   r;   )r<   ts     r   
<listcomp>z%summarize_holders.<locals>.<listcomp>]   s    WWWQUVWqWWWr(   c              3  Z   K   | ]&}t          |                    d                     V  'dS )
started_atN)_optional_floatr#   )r<   es     r   r>   z$summarize_holders.<locals>.<genexpr>]   s6      QQA?155+>+>??QQQQQQr(   z	, oldest z ago)r   r#   joinsorteditemsr2   timemin)r3   countsentryr7   heldstarteds         r   summarize_holdersrR   Q   s	    rF 5 5eii	**7i88 **Wa0014w99   5O5OPPP    D XWQQQQQWWWG IHJty{{S\\'ABBHHHHKr(   active_countr   max_sessionsOptional[list[dict[str, Any]]]c                P    t          |pg           }|rd| dnd}d|  d| d| dS )Nz
 Held by: .r6   z'Hermes is at the active session limit (/z).z) Try again when another session finishes.)rR   )rS   rT   r3   rP   details        r   active_session_limit_messagerZ   c   sd     W]++D%)1!$!!!!rF	=, 	= 	= 	= 	=	= 	= 	=r(   r   c                 >    t          t                                dz  S )Nruntime)r   r   r;   r(   r   
_state_dirr]   u   s    !!""Y..r(   c                 $    t                      dz  S )Nzactive_sessions.jsonr]   r;   r(   r   _state_pathr`   y       <<000r(   c                 $    t                      dz  S )Nzactive_sessions.lockr_   r;   r(   r   
_lock_pathrc   }   ra   r(   c                  "    e Zd ZddZd Zd ZdS )	_FileLockpathr   c                "    || _         d | _        d S N)rf   _fh)selfrf   s     r   __init__z_FileLock.__init__   s    	r(   c                   | j         j                            dd           t          | j         d          | _        t
          j        dk    r	 dd l}| j                            d           |	                    | j        
                                |j        d           n# t          $ r5}| j                                         d | _        t          d          |d }~ww xY w	 dd l}|                    | j        
                                |j                   nB# t          $ r5}| j                                         d | _        t          d          |d }~ww xY w| S )NTparentsexist_okza+bntr   r9   z$active session file lock unavailable)rf   parentmkdiropenri   osnamemsvcrtseeklockingfilenoLK_LOCK	ExceptioncloseRuntimeErrorfcntlflockLOCK_EX)rj   rv   excr~   s       r   	__enter__z_FileLock.__enter__   s_   	td;;;	5))7d??Ta   tx00&.!DDDD T T T   "#IJJPSST
TDHOO--u}==== T T T   "#IJJPSST s1   AB 
C)0CC"6D 
E#0EEc                   | j         d S t          j        dk    rc	 dd l}| j                             d           |                    | j                                         |j        d           nX# t          $ r Y nLw xY w	 dd l	}|
                    | j                                         |j                   n# t          $ r Y nw xY w	 | j                                          d | _         d S # d | _         w xY w)Nrp   r   r9   )ri   rt   ru   rv   rw   rx   ry   LK_UNLCKr{   r~   r   LOCK_UNr|   )rj   exc_typer   tbrv   r~   s         r   __exit__z_FileLock.__exit__   s   8F7d??a   tx00&/1EEEE   DHOO--u}====   	HNNDHHHtDHOOOOs0   AA- -
A:9A:>6B5 5
CCC( (	C1N)rf   r   )__name__
__module____qualname__rk   r   r   r;   r(   r   re   re      sF             0    r(   re   rf   c                   	 t          | dd          5 }t          j        |          }d d d            n# 1 swxY w Y   n:# t          $ r g cY S t          $ r  t
                              d|            g cY S w xY wt          |t                    r|	                    d          n|}t          |t                    sg S d |D             S )Nrutf-8encodingz.Ignoring corrupt active session registry at %sr3   c                <    g | ]}t          |t                    |S r;   )r   r"   r<   rO   s     r   rD   z!_read_entries.<locals>.<listcomp>   s'    BBBe*UD*A*ABEBBBr(   )rs   jsonloadFileNotFoundErrorr{   r   r   r   r"   r#   list)rf   fhdatar3   s       r   _read_entriesr      s"   $g... 	!"9R==D	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	!   			   GNNN			 &0d%;%;Edhhy!!!Ggt$$ 	BBwBBBBs2   A 5A 9A 9A A8&A87A8Nonec                   | j                             dd           |                     | j         dt	          j                     dt          j                    j         d          }t          |dd          5 }t          j        d|i|d	           d d d            n# 1 swxY w Y   t	          j        ||            d S )
NTrm   rW   z.tmpwr   r   r3   )	sort_keys)rq   rr   	with_nameru   rt   getpiduuiduuid4hexrs   r   dumpreplace)rf   r3   tmpr   s       r   _write_entriesr      s    KdT222
..DILL	LLdjll6FLLL
M
MC	c3	)	)	) <R	9g&d;;;;< < < < < < < < < < < < < < <JsDs   9BB#&B#pidOptional[float]c                    	 dd l }t          |                    |                                                     S # t          $ r Y d S w xY w)Nr   )psutilr   Processcreate_timer{   )r   r   s     r   _process_start_timer      sX    V^^C((4466777   tts   7: 
AAc                f    | | dk    rd S 	 t          |           S # t          t          f$ r Y d S w xY w)Nr6   )r   r   r   )r
   s    r   rG   rG      sL    }tU||z"   tts    00process_start_timer   c                F   	 t          |           }n# t          t          f$ r Y dS w xY w|dk    rdS 	 ddlm} t           ||                    }n# t          $ r Y dS w xY w|sdS t          |          }|dS t          |          }|dS t          ||z
            dk     S )NFr   )_pid_existsTgMbP?)
r   r   r   gateway.statusr   r   r{   rG   r   abs)r   r   pid_intr   existsexpected_startcurrent_starts          r   
_pid_aliver      s    c((z"   uu!||u......kk'**++   uu u$%788Nt'00Mt}~-..66s    ''A 
A A c                    d | D             S )Nc                ~    g | ]:}t          |                    d           |                    d                    8|;S )r   r   )r   r#   r   s     r   rD   z_prune_dead.<locals>.<listcomp>   sQ       eii&&		2F(G(GHH  r(   r;   )r3   s    r   _prune_deadr      s#        r(   c                  R    e Zd ZU ded<   ded<   ded<   dZded<   dZded	<   ddZdS )ActiveSessionLeaser   lease_id
session_idr7   Tr   enabledFreleasedr   r   c                D    | j         s| j        sd S t          |            d S rh   )r   r   release_active_session)rj   s    r   releasezActiveSessionLease.release	  s.    = 	 	Ft$$$$$r(   N)r   r   )r   r   r   __annotations__r   r   r   r;   r(   r   r   r     sg         MMMOOOLLLGH% % % % % %r(   r   )metadatar   r7   r   Optional[dict[str, Any]]2tuple[Optional[ActiveSessionLease], Optional[str]]c                .   t          |          }t          j                    j        }|t	          || |d          dfS t          j                    }|t          |           t          |          t          j                    t          t          j                              ||d}|r!d |
                                D             |d<   t                      }t          t                                5  t          |          }	t          |	          }
t!          |	          t!          |
          z
  }|rt"                              d|           t!          |
          }||k    rLt'          ||
           t"                              d|||           dt)          |||
          fcddd           S |
                    |           t'          ||
           ddd           n# 1 swxY w Y   t	          |t          |           t          |          	          dfS )
zAcquire an active-session slot.

    Returns ``(lease, None)`` on success.  When the cap is disabled, the lease is
    a no-op object so callers can unconditionally call ``release()``.
    NF)r   r   r7   r   )r   r   r7   r   r   rF   
updated_atc                ^    i | ]*\  }}t          |t                    t          |          |+S r;   r   r   r<   kvs      r   
<dictcomp>z.try_acquire_active_session.<locals>.<dictcomp>0  sC     
 
 
!QjC6H6H
FFA
 
 
r(   r   z'Pruned %d stale active session lease(s)z9Active session limit reached: active=%d max=%d surface=%s)r   r   r7   )r'   r   r   r   r   rL   r   rt   r   r   rK   r`   re   rc   r   r   lenr   infor   rZ   append)r   r7   r   r   rT   r   nowrO   
state_pathraw_entriesr3   prunedrS   s                r   try_acquire_active_sessionr     s    36::Lz||H!!	
 
 

  	 )++C*oow<<y{{1")++>> E  

 
"*.."2"2
 
 
j J	:<<	 	  , ,#J//k**[!!CLL0 	KKKA6JJJ7||<'':w///KKK	   5lG  , , , , , , , ,$ 	uz7+++', , , , , , , , , , , , , , ,* z??G   	 s   1B/G-%GG"%G"leasec                f    t                      }	 t          t                                5  t          t	          |                    } fd|D             }t          |          t          |          k    rt          ||           d d d            n# 1 swxY w Y   d _        d S # d _        w xY w)Nc                n    g | ]1}t          |                    d           pd          j        k    /|2S )r   r6   )r   r#   r   )r<   rO   r   s     r   rD   z*release_active_session.<locals>.<listcomp>V  sI       uyy,,233u~EE EEEr(   T)r`   re   rc   r   r   r   r   r   )r   r   r3   kepts   `   r   r   r   Q  s    Jz||$$ 	1 	1!-
";";<<G   $  D
 4yyCLL((z4000	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 s/   B' ABB' BB' BB' '	B0c               <   t          |pd          }|sdS | j        rdS | j        s	|| _        dS t	                      }t          t                                5  t          t          |                    }d}|D ]p}t          |	                    d          pd          | j
        k    r0||d<   t          j                    |d<   |r!d |                                D             |d<   d} |rt          ||           || _        |cd	d	d	           S # 1 swxY w Y   d	S )
zEMove an existing lease to a new session id without dropping the slot.r6   FTr   r   r   c                ^    i | ]*\  }}t          |t                    t          |          |+S r;   r   r   s      r   r   z+transfer_active_session.<locals>.<dictcomp>{  sC     % % %"&!QjC>P>P%FFA% % %r(   r   N)r   r   r   r   r`   re   rc   r   r   r#   r   rL   rK   r   )r   r   r   new_session_idr   r3   updatedrO   s           r   transfer_active_sessionr   a  s    )r**N u~ u= )tJ	:<<	 	   mJ7788 
	 
	E599Z((.B//5>AA"0E,"&)++E, % %*2..*:*:% % %j! G 	.:w///-E#                 s   B,DDDlive_lease_idsset[str]c                    t          j                    t                      }|                                sdS t	          t                                5  t          t          |                    } fd|D             }t          |          t          |          z
  }|rt          ||           ddd           n# 1 swxY w Y   |S )u  Drop this process's registry entries that no live session owns.

    ``_prune_dead`` only reclaims leases whose owning process died. A server
    that runs for days (``hermes dashboard`` / ``serve``) never trips that
    check, so a lease whose session skipped teardown is held until restart.
    The owning process is the only authority on which of its own leases are
    real, so it drops the rest itself — exact, with no heartbeat write on the
    turn path and no staleness threshold to tune.
    r   c                    g | ]C}|                     d           k    s&t          |                     d          pd          v A|DS )r   r   r6   )r#   r   )r<   rO   r   r   s     r   rD   z+release_orphaned_leases.<locals>.<listcomp>  sa     
 
 
yy3&&599Z((.B//>AA  BAAr(   N)
rt   r   r`   r   re   rc   r   r   r   r   )r   r   r3   r   droppedr   s   `    @r   release_orphaned_leasesr     s    )++CJ  q	:<<	 	  
- 
-mJ7788
 
 
 
 
 
 
 
 g,,T* 	-:t,,,
- 
- 
- 
- 
- 
- 
- 
- 
- 
- 
- 
- 
- 
- 
- Ns   AB>>CCc                     t                      } t          t                                5  t          t	          |                     }t          | |           |cddd           S # 1 swxY w Y   dS )z@Return the pruned active-session registry for diagnostics/tests.N)r`   re   rc   r   r   r   )r   r3   s     r    active_session_registry_snapshotr     s    J	:<<	 	   mJ7788z7+++                 s   .A%%A),A))r	   )r
   r   r   r   r   r   )r   r   r   r   )r)   r   r   r   )r3   r4   r   r   rh   )rS   r   rT   r   r3   rU   r   r   )r   r   )rf   r   r   r4   )rf   r   r3   r4   r   r   )r   r   r   r   )r
   r   r   r   )r   r   r   r   r   r   )r3   r4   r   r4   )
r   r   r7   r   r   r   r   r   r   r   )r   r   r   r   )r   r   r   r   r   r   r   r   )r   r   r   r   )r   r4   ))__doc__
__future__r   r   loggingrt   rL   r   dataclassesr   pathlibr   typingr   r   hermes_constantsr   	getLoggerr   r   r   r'   r2   rR   rZ   r]   r`   rc   re   r   r   r   rG   r   r   r   r   r   r   r   r   r;   r(   r   <module>r      s    # " " " " "   				   ! ! ! ! ! !                       , , , , , ,		8	$	$    @8 8 8 8"A A A A   * /3    $/ / / /1 1 1 11 1 1 12 2 2 2 2 2 2 2jC C C C         7 7 7 7 70    
% 
% 
% 
% 
% 
% 
% 
%$ *.? ? ? ? ? ?D   ( *.	" " " " " "J   <     r(   