
    }-jF                    Z   d Z ddlZddlZddlZddl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 ddlmZmZmZmZmZmZmZmZmZmZmZmZmZ ddlmZ ddl m!Z!m"Z"  ej#        e$          Z%dZ&d	Z' ej(                    Z)d]dZ*d^de+d
e,fdZ-d^dZ.d Z/d Z0d Z1d Z2d Z3d Z4d Z5d_dZ6d Z7 G d d          Z8d
efdZ9	 d^ddd
e,fdZ:dZ;d
e,fd Z<d`d"Z=dad#Z>d$e?d
e,fd%Z@d
efd&ZAd'eBe?         d
dfd(ZCd)e+d
dfd*ZDd^d+ZEdbd-e+fd.ZF ejG                    ZHd!aId/eBd
e,fd0ZJdd1d2e,d
eBfd3ZKd]d4ZLd5ZM ejG                    ZNd
e+fd6ZOd^de+d7e+d
e,fd8ZP	 	 	 	 	 dcde+d:e+d;e+fd<ZQd=ZRd>ZSd?ZTd@ZUdAZVdBZWd
edz  fdCZXdDed
e+fdEZYdF ZZd
e+fdGZ[eVdHdDedIe\d
e]eBe?         e+dz  e^dz  e^dz  f         fdJZ_d^eTeUdKdLee+z  dz  dMe\dNe\fdOZ`dPeBe?         dQe+dz  d
e+fdRZad^eTeUdKdLee+z  dz  dMe\dNe\d
eBfdSZbd^dLee+z  dz  d
eBfdTZcd
eBfdUZddV Zed
eBfdWZfd^dXe+dYe^dz  d
e\fdZZgd[Zhd
e,fd\ZidS )dz;Hermes Web UI -- Session model and in-memory session store.    N)closing)Path)SESSION_DIRSESSION_INDEX_FILESESSIONSSESSIONS_MAXLOCKSTREAMSSTREAMS_LOCKDEFAULT_WORKSPACEDEFAULT_MODELPROJECTS_FILEHOMEget_effective_default_model_get_session_agent_lock)get_last_workspace)"read_importable_agent_session_rowsread_session_lineage_metadata   i  returnc                  V   t          j                     t          z
  } 	 t          j        d          D ]f}	 |                                j        | k     r6|                    d           t                              d|j	                   W# t          $ r Y cw xY wdS # t          $ r Y dS w xY w)u8  Best-effort removal of stale ``*.tmp.*`` files from SESSION_DIR.

    Only files whose mtime is older than ``_STALE_TMP_AGE_SECONDS`` are
    removed so that in-flight writes from a long-running sibling process
    are not disturbed.  Errors are logged and swallowed — this must never
    prevent startup.
    z*.tmp.*T
missing_okzCleaned up stale tmp file: %sN)time_STALE_TMP_AGE_SECONDSr   globstatst_mtimeunlinkloggerdebugnameOSError	Exception)cutoffps      /root/hermes-webui/api/models.py_cleanup_stale_tmp_filesr(   .   s     Y[[11F	!),, 	 	A6688$v--HHH---LL!@!&III   	 	    s5   B ABB 
BB BB 
B('B(
session_idc                     | sdS |?t           5  t          t          j                              }ddd           n# 1 swxY w Y   | |v rdS t          |  dz  }|                                S )a<  Return True if an index entry still has backing state.

    A session can legitimately exist either as a persisted JSON file or as an
    in-memory Session object that has not been flushed yet.  This helper is used
    to prune stale `_index.json` rows left behind after session-id rotation or
    file removal.
    FNT.json)r	   setr   keysr   exists)r)   in_memory_idsr&   s      r'   _index_entry_existsr0   C   s      u 	1 	100M	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1 	1]""t****A88::s   !;??c                 	   t          j        dt          j                     dt	          j                    j                   }t          5  | t          j                    s0t                       g }t          j        d          D ]}|j                            d          r	 t                              |j                  }|r'|                    |                                           g# t&          $ r t(                              d|           Y w xY wt,          5  d |D             }t/          j                    D ]2}|j        |vr'|                    |                                           3|                    d d	
           t7          j        |dd          }ddd           n# 1 swxY w Y   	 t;          |dd          5 }|                    |           |                                 t          j         |!                                           ddd           n# 1 swxY w Y   t          j"        |t                      n7# t&          $ r* 	 |#                    d	           n# t&          $ r Y nw xY w w xY w	 ddd           dS d}	 t,          5  t7          j$        t          j%        d                    }	tM          t/          j'                              d t          j        d          D             fd|	D             }	d | D             }
d |	D             }|
(                                D ]\  }}||vr|	                    |           tS          |	          D ])\  }}|*                    d          }||
v r|
|         |	|<   *|	                    d d	
           t7          j        |	dd          }ddd           n# 1 swxY w Y   	 t;          |dd          5 }|                    |           |                                 t          j         |!                                           ddd           n# 1 swxY w Y   t          j"        |t                      n7# t&          $ r* 	 |#                    d	           n# t&          $ r Y nw xY w w xY wn# t&          $ r d	}Y nw xY wddd           n# 1 swxY w Y   |rtW          d           dS dS )u  Update the session index file.

    When *updates* is provided (a list of Session objects whose compact
    entries should be refreshed), this does a targeted in-place update of
    the existing index — O(1) for single-session changes.  When *updates*
    is None, a full rebuild is performed (used on startup / first call).

    LOCK protects in-memory state snapshots and payload construction only;
    disk I/O (write/flush/fsync/replace) always runs outside LOCK.
    .tmp..N*.json_Failed to load session from %sc                 8    h | ]}|                     d           S r)   get.0es     r'   	<setcomp>z'_write_session_index.<locals>.<setcomp>s   s$    EEEl 3 3EEE    c                 .    |                      dd          S N
updated_atr   r9   ss    r'   <lambda>z&_write_session_index.<locals>.<lambda>w   s    155q+A+A r?   TkeyreverseF   ensure_asciiindentwutf-8encodingr   c                 P    h | ]#}|j                             d           |j        $S )r5   )r"   
startswithstemr<   r&   s     r'   r>   z'_write_session_index.<locals>.<setcomp>   s@       6,,S11F  r?   c                 p    g | ]2}|                     d           v s|                     d           v 0|3S r8   r9   )r<   r=   r/   on_disk_idss     r'   
<listcomp>z(_write_session_index.<locals>.<listcomp>   sQ       l++}<<l@S@SWb@b@b @b@b@br?   c                 B    i | ]}|j         |                                S  )r)   compactr<   rD   s     r'   
<dictcomp>z(_write_session_index.<locals>.<dictcomp>   s$    JJJQq|QYY[[JJJr?   c                 8    h | ]}|                     d           S r8   r9   r;   s     r'   r>   z'_write_session_index.<locals>.<setcomp>   s$    FFFl 3 3FFFr?   r)   c                 .    |                      dd          S rA   r9   rC   s    r'   rE   z&_write_session_index.<locals>.<lambda>   s    AEE,,B,B r?   updates),r   with_suffixosgetpid	threadingcurrent_threadident_INDEX_WRITE_LOCKr.   r(   r   r   r"   rR   SessionloadrS   appendrZ   r$   r    r!   r	   r   valuesr)   sortjsondumpsopenwriteflushfsyncfilenoreplacer   loads	read_textr,   r-   items	enumerater:   _write_session_index)r`   _tmpentriesr&   rD   existing_ids_payloadf	_fallbackexistingupdated_mapsidentryir=   r/   rV   s                  @@r'   ry   ry   V   s    )*b")++*b*b	@X@Z@Z@`*b*bccD	 W W?"4";"="=?$&&&G %h// F F6$$S)) FQV,,A 4qyy{{333  F F FLL!A1EEEEEF  M MEEWEEE!** 4 4A|<77qyy{{333!A!A4PPP:gE!LLLM M M M M M M M M M M M M M M$g666 )!GGH%%%GGIIIHQXXZZ((() ) ) ) ) ) ) ) ) ) ) ) ) ) ) 
4!34444   KK4K0000    D IW W W W W W W WP 	.	 N N:&8&BG&T&T&TUU #HMOO 4 4 (-h77      '   KJ'JJJFFXFFF"-"3"3"5"5 / /JC,.. ...%h// 7 7DAq%%--Ck))&1#&6"B"BDQQQ:hU1MMM=N N N N N N N N N N N N N N N@$g666 )!GGH%%%GGIIIHQXXZZ((() ) ) ) ) ) ) ) ) ) ) ) ) ) ) 
4!34444   KK4K0000    D 5  	 	 	III	mW W W W W W W W W W W W W W Wr  +T******+ +sn  AS$AC-,S-%DSD
SBF-!S-F1	1S4F1	5S9IAH'I'H+	+I.H+	/IS
JI/.J/
I<9J;I<<JSSR(D(OR(O	R(O	R(Q0.AQ
>Q0
Q	Q0Q	Q0/R(0
R$;RR$
RR$RR$$R('S(R74S6R77SS
S
c                      t           5  t          t          j                              cd d d            S # 1 swxY w Y   d S N)r   r,   r
   r-   rY   r?   r'   _active_stream_idsr      s}    	 # #7<>>""# # # # # # # # # # # # # # # # # #s    599c                 (    t          | o| |v           S r   )bool)active_stream_idactive_stream_idss     r'   _is_streaming_sessionr      s     J%59J%JKKKr?   c                     t          | t                    r,|                     d          p|                     d          pdS t          t	          | dd                     pt	          | dd          pdS )Nlast_message_atrB   r   messages)
isinstancedictr:   _last_message_timestampgetattr)sessions    r'   _session_sort_timestampr      sr    '4   P{{,--O\1J1JOaO"77J#E#EFFp'RY[gijJkJkpoppr?   c                     t          | t                    sd S |                     d          p|                     d          }	 |t          |          nd S # t          t
          f$ r Y d S w xY w)N_ts	timestamp)r   r   r:   float	TypeError
ValueError)messageraws     r'   _message_timestampr      s{    gt$$ t
++e


8K 8 8C _uSzzz$6z"   tts   A A+*A+c                     t          | t                    sd S t          |           D ]F}t          |t                    r|                    d          dk    r1t          |          }|r|c S Gd S )Nroletool)r   listreversedr   r:   r   )r   r   tss      r'   r   r      s    h%% tH%%  gt$$ 	V)<)<)F)F(( 	III	4r?   c                     t          | t                    sdS t          |                     dd                                                                                    S )N r   )r   r   strr:   striplower)r   s    r'   _message_roler      sM    gt$$ rw{{62&&''--//55777r?   c                 "   d}d}t          |           }||k     r| |         }|dk    r|}|dz  }d}g }||k     rR| |         }	|r|                    |	           d}n%|	dk    rd}n|	dk    rn |                    |	           |dz  }||k     R||k    rdS |dk    rWd                    |          |k    r>|dz   }
|
|k     r| |
         d	v r|
dz  }
|
|k     r
| |
         d	v |
|k     r| |
         d
k    r|S n|dv r|dz  }n	|dv r|dz  }|dz  }||k     dS )zBReturn the byte offset of a top-level JSON object key, if present.r   "   F\TNr   z 	
:z{[z}])lenrj   join)textrG   depthr   nchstartescapedcharscjs              r'   _find_top_level_json_keyr      s   E	AD		A
a%%!W99EFAGEa%%G $LLOOO#GG$YY"GG#XXLLOOOQ a%% Avvtzzbggenn33E!eeQ9 4 4FA !eeQ9 4 4q55T!W^^ L4ZZQJEE4ZZQJE	Q? a%%@ 4r?      c                    d}t          | dd          5 }t          |                    d                    |k     r|                    d          }|s	 ddd           dS ||z  }t	          |d          }|c|d|                                         }|                    d          r|dd	                                         }| d
cddd           S 	 ddd           n# 1 swxY w Y   dS )zCRead only the metadata portion before the top-level messages array.r   rrN   rO   i   Nr   ,z
})ro   r   encodereadr   rstripendswith)pathmax_prefix_bytesbufr~   chunkmessages_posprefixs          r'   _read_metadata_json_prefixr     su   
C	dC'	*	*	* #a#**W%%&&)999FF4LLE 	# # # # # # # #
 5LC3CDDL#,'..00Fs## .++--???# # # # # # # #9# # # # # # # # # # # # # # # 4s   ?C$!A)C$$C(+C(c                    	 t          j        t          j        d                    }n# t          $ r Y dS w xY wt          |t                    sdS |D ]}|                    d          | k    r|                    d          }t          |t                    r
|dk    r|c S 	 t          |          }n# t          t          f$ r Y  dS w xY w|dk    r|ndc S dS )zGReturn the indexed message count without loading the full session file.rN   rO   Nr)   message_countr   )rm   ru   r   rv   r$   r   r   r:   intr   r   )r)   r{   r   counts       r'   _lookup_index_message_countr   &  s   */97KKKLL   ttgt$$ t 
- 
-99\""j00		/**eS!! 	eqjjLLL	JJEE:& 	 	 	444	

uu,,,4s   '* 
88%B55C
Cc            &          e Zd Zdd ee          eddddddddddddddddddddddddddddddddddf&dedededed	ed
edededededefdZe	d             Z
ddededdfdZed             Zed             ZddefdZdS )rh   NUntitledFr   r)   titlepinnedarchived
project_idinput_tokensoutput_tokensr   pending_user_messagellm_title_generatedparent_session_idc'                    |pt          j                    j        d d         | _        || _        t          t          |                                                                                    | _	        || _
        |r3t          |                                                                          nd | _        |pg | _        |	pg | _        |pt!          j                    | _        |pt!          j                    | _        t'          |
          | _        t'          |          | _        |pd | _        || _        |pd| _        |pd| _        || _        || _        || _        || _        |pg | _        || _        tA          |tB                    r|ng | _"        || _#        || _$        || _%        || _&        || _'        || _(        tA          |tR                    r|nd | _*        tA          |tB                    r|ng | _+        t'          |          | _,        | | _-        |!r@t          t          |!                                                                                    nd | _.        |"rt          |"          nd | _/        |#r@t          t          |#                                                                                    nd | _0        |$| _1        t'          |'2                    dd                    | _3        |'2                    d          | _4        |'2                    d          | _5        |'2                    d          | _6        |'2                    d          | _7        |%| _8        tA          |&tR                    r|&ni | _9        d | _:        d S )	N   r   is_cli_sessionF
source_tag
raw_sourcesession_sourcesource_label);uuiduuid4hexr)   r   r   r   
expanduserresolve	workspacemodelr   r   model_providerr   
tool_callsr   
created_atrB   r   r   r   r   profiler   r   estimated_costpersonalityr   r   pending_attachmentspending_started_atr   r   context_messagescompression_anchor_visible_idxcompression_anchor_message_keycompression_anchor_summarycontext_lengththreshold_tokenslast_prompt_tokensr   gateway_routinggateway_routing_historyr   r   worktree_pathworktree_branchworktree_repo_rootworktree_created_atr:   r   r   r   r   r   enabled_toolsetscomposer_draft_metadata_message_count)(selfr)   r   r   r   r   r   r   rB   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   kwargss(                                           r'   __init__zSession.__init__=  s%   8 %=
(8"(=
T)__7799AACCDD
ES]c.117799??AAAY] B$*$3	$3	6llX$,(-A*/a,& 0$8!#6#<" "44>?OQU4V4V ^ 0 0\^.L+.L+*D', 0"42<_d2S2S]Y]BLMdfjBkBk's'>'>qs$#'(;#<#< !2P]gSm!4!4!?!?!A!A!I!I!K!KLLLcg7FPs?333DZl"v#d+=&>&>&I&I&K&K&S&S&U&U"V"V"Vrv#6 "6::.>#F#FGG **\22 **\22$jj)9::"JJ~66 00:>40P0PXnnVX'+$$$r?   c                 &    t           | j         dz  S )Nr+   )r   r)   )r  s    r'   r   zSession.path  s    66666r?   Ttouch_updated_at
skip_indexr   c                 l    t           dd          rt          d j        d          |rt          j                     _        g d fdD             } j        |d<    j        |d<   fd	 j                                        D             }t          j
        i ||dd
          }	  j                                        r j                            d          }	 t          j        |          }t          |                    d          pg           }n# t          j        t$          f$ r d}Y nw xY wt           j        pg           }	||	k    r$ j                            d          }
	 |
                    dt)          j                     dt-          j                    j                   }t3          |dd          5 }|                    |           |                                 t)          j        |                                           d d d            n# 1 swxY w Y   t)          j        ||
           n8# t>          $ r+ 	 |                     d           n# tB          $ r Y nw xY wY nw xY wn# t>          $ r Y nw xY w j                            dt)          j                     dt-          j                    j                   }	 t3          |dd          5 }|                    |           |                                 t)          j        |                                           d d d            n# 1 swxY w Y   t)          j        | j                   n7# tB          $ r* 	 |                     d           n# tB          $ r Y nw xY w w xY w|stE           g           d S d S )N_loaded_metadata_onlyFz'Refusing to save metadata-only session zx: would atomically overwrite on-disk messages with []. Reload with metadata_only=False before mutating state. See #1558.)(r)   r   r   r   r   r   rB   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   c                 4    i | ]}|t          |d           S r   )r   )r<   kr  s     r'   r\   z Session.save.<locals>.<dictcomp>  s'    CCCa74D))CCCr?   r   r   c                 V    i | ]%\  }}|v	|d v|                     d          "||&S ))r   r   r5   )rR   )r<   r  vMETADATA_FIELDSs      r'   r\   z Session.save.<locals>.<dictcomp>  sS     , , ,$!Q_,,:T1T1Tc** 2U A1T1T1Tr?   rI   rJ   rN   rO   r   z	.json.bakz	.bak.tmp.r3   rM   Tr   r2   r_   )#r   RuntimeErrorr)   r   rB   r   r   __dict__rw   rm   rn   r   r.   rv   ru   r   r:   JSONDecodeErrorr   ra   rb   rc   rd   re   rf   ro   rp   rq   rr   rs   rt   r#   r   r$   ry   )r  r  r  metaextrapayloadexisting_textr   existing_msg_countincoming_msg_countbak_pathbak_tmpbftmpr~   r  s   `              @r'   savezSession.save  s    40%88 	$/       	*"ikkDO
 
 
 DCCC?CCC=Z!_\, , , ,$-"5"5"7"7 , , , *...U1MMM#	y!!  ! $	 3 3W 3 E E,#z-88H),X\\*-E-E-K)L)L&&,j9 , , ,)+&&&,%()<"%=%="%(:::#y44[AAH!"*"6"6X	XXi6N6P6P6VXX# # "'3AAA 2RHH]333HHJJJHRYY[[1112 2 2 2 2 2 2 2 2 2 2 2 2 2 2 
7H5555" ! ! !!#NNdN;;;;( ! ! ! D!	!  	 	 	D	 i##$\BIKK$\$\):R:T:T:Z$\$\]]	c3111 %Q   			$$$% % % % % % % % % % % % % % % JsDI&&&& 	 	 	

d
++++   	  	1 $000000	1 	1s   -5I< #8D I< D74I< 6D77:I< 2AI AH"I "H&&I )H&*I I< 
I8I%$I8%
I2/I81I22I85I< 7I88I< <
J	J	M( &AM6M( MM( 	M
M( (
N3N
	N

NNNNc           	          |rt          d |D                       sd S t          | dz  }|                                sd S  | di t          j        |                    d                    S )Nc              3      K   | ]}|d v V  	dS %0123456789abcdefghijklmnopqrstuvwxyz_NrY   r<   r   s     r'   	<genexpr>zSession.load.<locals>.<genexpr>  (      XXqa#JJXXXXXXr?   r+   rN   rO   rY   )allr   r.   rm   ru   rv   )clsr   r&   s      r'   ri   zSession.load  s      	#XXTWXXXXX 	4S---'xxzz 	4s??TZW = =>>???r?   c                     |rt          d |D                       sdS t          | dz  }|                                sdS 	 t          |          }|s|                     |          S t          j        |          }h d}|                    |                                          s|                     |          S g |d<   g |d<    | di |}t          |          |_
        d|_        |S # t          $ r |                     |          cY S w xY w)	a  Load only the compact metadata fields, skipping the messages array.

        Session JSON files have metadata fields (session_id, title, model, etc.)
        at the top level, before the large messages array. Read only up to the
        top-level "messages" field and synthesize a small metadata-only object.
        Falls back to load() for legacy or unexpected file layouts.
        c              3      K   | ]}|d v V  	dS r  rY   r!  s     r'   r"  z-Session.load_metadata_only.<locals>.<genexpr>  r#  r?   Nr+   >   r   r   r)   rB   r   r   TrY   )r$  r   r.   r   ri   rm   ru   issubsetr-   r   r  r	  r$   )r%  r   r&   r   parsedneededr   s          r'   load_metadata_onlyzSession.load_metadata_only  s:     	#XXTWXXXXX 	4S---'xxzz 	4	!/22F %xx}}$Z''FHHHF??6;;==11 %xx}}$!#F:#%F< cmmFmmG.I#.N.NG+ -1G)N 	! 	! 	!88C==   	!s   %C+ (AC+ <.C+ +DDc                 ,   ||nt                      }t          | j                  }| j        | j        nt	          | j                  }|rt          |d          }t          | j                  p| j        }|r| j	        r| j	        }i d| j
        d| j        d| j        d| j        d| j        d|d| j        d	| j        d
|d| j        d| j        d| j        d| j        d| j        d| j        d| j        d| j        | j        | j        | j        | j        | j        | j        | j        | j        d| j         r	d| j         ini | j!        r| j!        | j"        | j#        | j$        dni tK          | j        tL                    rtO          d | j        D                       nd| j(        | j        || j)        | j*        | j+        | j,        | j-        | j.        tK          | j/        t`                    r| j/        ni |rtc          | j(        |          nddS )Nr   r)   r   r   r   r   r   r   rB   r   r   r   r   r   r   r   r   r   )r   r   r   r   r   r   r   r   r   )r   r   r   r   c              3   @   K   | ]}t          |          d k    dV  dS )userr   N)r   )r<   r   s     r'   r"  z"Session.compact.<locals>.<genexpr>Z  s=       & &-2H2HF2R2R2R2R2R2R& &r?   r   F)user_message_countr   r   has_pending_user_messager   r   r   r   r   r   r   is_streaming)2r,   r   r   r  r   r   maxr   rB   r   r)   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   sumr   r   r   r   r   r   r   r   r   r   )r  include_runtimer   r0  r   r   s         r'   rZ   zSession.compact*  s   1B1N--TWTYTY#'(A#B#B  +7 ((T]## 	
 $ 	2q11M1$-@@SDO# 	6(? 	6"5O3
$/3
TZ3
 3
 TZ	3

 d13
 ]3
 $/3
 $/3
 3
 dk3
 3
 $/3
 t|3
 D-3
 T/3
  d1!3
" 4+#3
$ /3.Q.2.Q*.*I"1 $ 5"&"9#3'+'C33
 3
 3
8 AE@V^#T%;<<\^93
D #,!%!3#'#7&*&='+'?	  
 *,E3
J DM400#8# & &!%& & & # # #67 $ 5$($=(@"1//"1 - $ 55?@SUY5Z5Zbd11`b !,1%'8  &+e3
 3
 3
 3	
r?   )TF)FN)__name__
__module____qualname__r   r   r   r   r   r  propertyr   r  classmethodri   r+  r   rZ   rY   r?   r'   rh   rh   <  s       '+
3011 $4D uU!%t#$4!'++/%)$("&0404,0 $t$(!%t+0'+" $#'$(!%#5G, G,3 G,C G, +/	G, AE	G,
 !G,  #G, 69G, $'G, (+G,& '+'G,( $')G, G, G, G,R 7 7 X7n1 n1T n1d n1t n1 n1 n1 n1` @ @ [@ #! #! [#!J@
 @
 @
 @
 @
 @
 @
 @
r?   rh   c                     	 ddl m} t           ||                     S # t          $ rC t          t          j                            d          pd                                          cY S w xY w)zResolve the hermes agent home directory for the given profile.

    Prefers the profile-specific helper from api.profiles; falls back to the
    HERMES_HOME environment variable or ~/.hermes, expanding ~ correctly.
    r   )get_hermes_home_for_profileHERMES_HOMEz	~/.hermes)api.profilesr;  r   ImportErrorrb   environr:   r   )r   r;  s     r'   _get_profile_homer@  l  s    O<<<<<<//88999 O O OBJNN=11@[AALLNNNNNOs     A
A-,A-T)require_stream_deadc                >	   | j         }| j        sdS |&| j        |k    rdS |r| j        t                      v rdS t	          | j                  dk    r d                    t          | j        pd                                                    }d}|r| j        r| j        d         }t          |t                    rh|                    d          dk    rOd                    t          |                    d	          pd                                                    }||k    }t          t          j                              }	t          | j        t          t          f          r| j        dk    rt          | j                  }	|sDd| j        |	d
d}
| j        rt#          | j                  |
d<   | j                            |
           d| _        d| _        g | _        d| _        | j                            ddt          t          j                              d
d           |                                  t(                              d|           d
S |                                rt/          |d          5 }t1          j        |          }ddd           n# 1 swxY w Y   |                    dg           }|r|| _        |                    dg           | _        dD ].}|                    |          t7          | |||                    /d| _        d| _        g | _        d| _        |                                  t(                              d|t	          |                     d
S | j        rt          t          j                              }	t          | j        t          t          f          r| j        dk    rt          | j                  }	d| j        |	d
d}
| j        rt#          | j                  |
d<   | j                            |
           d| _        d| _        g | _        d| _        | j                            ddt          t          j                              d
d           |                                  t(                              d|           d
S )u0  Inner repair logic. Must be called with the per-session lock already held.

    Re-checks session state under the lock, then either syncs messages from the
    core transcript (if present and non-empty) or restores the pending user
    message as a recovered user turn and appends an error marker.

    stream_id_for_recheck: when provided, repair bails if session.active_stream_id
    changed (e.g. context compression rotated it).  The cache-miss repair path
    also requires the stream to be absent from active streams; the streaming
    thread's final fallback passes require_stream_dead=False because it runs
    before its own stream is removed from STREAMS.

    Returns True if repair was applied, False if the re-check bailed out.
    Must never raise — caller is responsible for exception handling.
    FNr    r   r   r   r.  contentT)r   rD  r   
_recoveredattachments	assistantz#**Previous turn did not complete.**)r   rD  r   _errorzPSession %s: recovered pending user turn (messages non-empty), added error markerrN   rO   r   r   )r   r   r   z3Session %s: synced %d messages from core transcriptz8Session %s: no core transcript found, added error marker)r)   r   r   r   r   r   r   r   splitr   r   r:   r   r   r   r   r   r   rj   r  r    infor.   ro   rm   ri   r   setattr)r   	core_pathstream_id_for_recheckrA  r   _pending_text_already_checkpointed	_last_msg
_last_text_recovered_ts	recoveredr~   corecore_messagesfields                  r'    _apply_core_sync_or_error_markerrW  y  s   , 
C' u(
 #'<<<5 	7#;?Q?S?S#S#S5 7!!W%A%GR!H!H!N!N!P!PQQ % 	DW- 	D(,I)T** Dy}}V/D/D/N/N XXc)--	*B*B*Hb&I&I&O&O&Q&QRR
(2m(C%DIKK((g03,?? 	<GD^abDbDb :;;M$ 		/"7*"	 I * M+/0K+L+L	-(##I...#' '+$&(#%)"<TY[[))	!
 !
 	 	 	 	^	
 	
 	
 t  )g... 	 !9Q<<D	  	  	  	  	  	  	  	  	  	  	  	  	  	  	 R00 	,G!%,!;!;GL 9 988E??.GUDK888'+G$+/G(*,G')-G&LLNNNKKES''   4 # + DIKK((g03,?? 	<GD^abDbDb :;;M3&	
 
	 & 	I'+G,G'H'HIm$	***#G#'G "$G!%G8%%	     LLNNN
KKJCPPP4s   8JJ J   c                    | j         }| j        r|r|t                      v rdS t          | dd          }|r	 t	          j                    t          |          z
  }n&# t          t          f$ r t          d          }Y nw xY w|t          k     r)t          
                    d| j        |t                     dS nt          d          }| j        }|rt          d |D                       sdS 	 t          | j                  }|dz  d| d	z  }t          |          }|                    d
          st          
                    d|           dS 	 d}|t          d          k    rdn|dd}	||k     rt          j        nt          j
        }
 |
d|||	           t%          | ||          |                                 S # |                                 w xY w# t(          $ r t                              d|           Y dS w xY w)uX  Recover a sidecar stuck with messages=[] and stale pending state.

    Fires only when messages is empty, pending_user_message is set,
    active_stream_id is set, the stream is no longer alive, AND the turn is
    older than _REPAIR_STALE_PENDING_GRACE_SECONDS (#1624).

    Uses a non-blocking lock acquire so a caller that already holds the
    per-session lock (e.g. retry_last, undo_last, cancel_stream) cannot
    deadlock when get_session() triggers this on a cache miss.

    Returns True if repair was applied, False otherwise.
    Must never raise — all errors are caught and logged.
    Fr   Ninfui   _repair_stale_pending: skipping repair for session %s — pending_started_at age=%.1fs < %ds grace windowc              3      K   | ]}|d v V  	dS r  rY   r!  s     r'   r"  z(_repair_stale_pending.<locals>.<genexpr>>  s(      TT1!FFTTTTTTr?   sessionssession_r+   )blockingzE_repair_stale_pending: lock contended, skipping repair for session %si,  z.1frD   zD_repair_stale_pending firing: session=%s stream_id=%s pending_age=%s)rM  z+_repair_stale_pending failed for session %s)r   r   r   r   r   r   r   r   #_REPAIR_STALE_PENDING_GRACE_SECONDSr    r!   r)   r$  r@  r   r   acquirewarningrW  releaser$   	exception)r   _seen_stream_id_started_ager   profile_homerL  lock_DIAG_WARN_WINDOW_SECONDS_age_str_logs              r'   _repair_stale_pendingrl    sp   " .O( ""4"6"666u
 w 4d;;H 	 9;;x0DD:& 	  	  	 <<DDD	 555LLB"D*M  
 5 6 U||

C cTTPSTTTTT u#(99 :-0E30E0E0EE	&s++ ||U|++ 	LLWY\   5	 ),%!%u!5!5d>>>>H%),E%E%E6>>6<DDV_h   4/   LLNNNNDLLNNNN   FLLLuus=   #A  A>=A>*A G AF< 'G <GG %G>=G>Fc                    t           5  | t          v r-t          j        |            t          |          cddd           S 	 ddd           n# 1 swxY w Y   |rt                              |           }|r|S nt                              |           }|r8t           5  |t          | <   t          j        |            t          t                    t          k    r2t          j        d           t          t                    t          k    2ddd           n# 1 swxY w Y   |s	 t          |          }|st          |j
                  dk    rm|j        rf|j        r_|j        t                      vrJt           5  t          j        |           |u rt          j        | d           ddd           n# 1 swxY w Y   n# t           $ r Y nw xY w|S t#          |           )ay  Load a session, optionally with metadata only (skipping the messages array).

    Metadata-only loads intentionally do not populate the full-session cache.
    Otherwise a later full load could return a compact object with an empty
    messages list. Use this when you only need compact() metadata and not the
    actual message history (e.g., for fast sidebar switching).
    NFlastr   )r	   r   move_to_endrh   r+  ri   r   r   popitemrl  r   r   r   r   r:   popr$   KeyError)r   metadata_onlyrD   repaireds       r'   get_sessionrv  g  s    
 ! !(?? %%%C=! ! ! ! ! ! ! !! ! ! ! ! ! ! ! ! ! ! ! ! ! !  &&s++ 	H	 LL  	- 	-HSM %%%h--,.. e,,,, h--,..	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	-
  	033
   4S__%9%92 &:. &: .6H6J6JJJ 4 4#<,,11$Ld3334 4 4 4 4 4 4 4 4 4 4 4 4 4 4    
3--s[   *AAAA.DDD$AF; 7,F/#F; /F33F; 6F37F; ;
GGc                 d   |$	 ddl m}  |            }n# t          $ r d}Y nw xY w|pt                      }t	          |t
                    r|nd}|r.|r*|                    d          r|                    d          n| n| }	t          |	pt                      |||||r|                    d          nd|r|                    d          nd|r|                    d          nd|r|                    d          nd	  	        }
t          5  |
t          |
j        <   t          j        |
j                   t          t                    t          k    r2t          j        d	
           t          t                    t          k    2ddd           n# 1 swxY w Y   |r|
                                 |
S )u  Create a new in-memory session.

    The session lives in the SESSIONS dict only — no disk write happens until
    the first message is appended (#1171 follow-up).  This avoids the
    "ghost Untitled session on disk" pile-up that occurred when users clicked
    New Conversation, reloaded the page, or completed onboarding without ever
    sending a message.  Subsequent code paths that populate state immediately
    (btw / background agent at api/routes.py) call ``s.save()`` themselves
    after setting title/messages, and ``_handle_chat_start`` saves the
    session as soon as the user actually sends a message — both are the
    natural first-write moments for a real session.

    Crash-safety: if the process exits between session creation and first
    message, the session is lost.  Since it had no messages, there is
    nothing to lose.  Worktree-backed sessions are the exception: they are
    saved immediately because creating the session also creates real
    filesystem state that must remain discoverable after restart.

    *profile* — when supplied by the caller (e.g. from the request body sent
    by the active browser tab), it is used directly so that concurrent clients
    on different profiles don't fight over a shared process-global.  If not
    supplied, we fall back to the process-level active profile (the pre-#798
    behaviour, preserved for calls that originate outside a request context).
    Nr   get_active_profile_namer   branch	repo_rootr   )	r   r   r   r   r   r   r   r   r   Frn  )r=  ry  r>  r   r   r   r:   rh   r   r	   r   r)   rp  r   r   rq  r  )r   r   r   r   r   worktree_infory  effective_modelwtworkspace_pathrD   s              r'   new_sessionr    s   2 	<<<<<<--//GG 	 	 	GGG	<:<<O$]D99	CtBQSbLrvvf~~LbffVnnn99YbN 8$6$8$8%(*4bffVnnn,.8x(((D24>266+...$46@BFF<000D
	 
	 
	A 
 ) )!"Q\***(mml**%(((( (mml**) ) ) ) ) ) ) ) ) ) ) ) ) ) )
 
 	Hs    $$A8FFFr   c                     t          |                     d          pd          }|                     d          p|                     d          }|dk    p|                    d          S )zJReturn True for internal/background sessions hidden from the default list.r)   r   r   sourcecroncron_)r   r:   rR   )r   r   r  s      r'   _hide_from_default_sidebarr    s^    
gkk,''-2
.
.C[[&&?'++h*?*?FV6s~~g666r?   c            
      h   	 ddl m}  t           |                                                                                       }nk# t
          $ r^ t          t          j        dt          t          dz                                                                                                }Y nw xY w|dz  S )zHReturn state.db for the active Hermes profile, degrading to HERMES_HOME.r   get_active_hermes_homer<  .hermesstate.db)
r=  r  r   r   r   r$   rb   getenvr   r   )r  hermes_homes     r'   _active_state_db_pathr    s    c777777113344??AAIIKK c c c29]Cy8H4I4IJJKKVVXX``bbc##s   AA A%B,+B,r\  c                     	 t          t                      d | D                       }n# t          $ r Y dS w xY w| D ]6}|                    d          }||v r|                    ||                    7dS )zFAttach state.db compression lineage metadata used by sidebar collapse.c                 8    h | ]}|                     d           S r8   r9   r[   s     r'   r>   z3_enrich_sidebar_lineage_metadata.<locals>.<setcomp>  s$    333QQUU<  333r?   Nr)   )r   r  r$   r:   update)r\  metadatar   r   s       r'    _enrich_sidebar_lineage_metadatar    s    0!##33(333
 
     * *kk,''(??NN8C=)))* *s   &) 
77r"   c                 \    | )	 |                      |           d S # t          $ r Y d S w xY wd S r   )stager$   )diagr"   s     r'   _diag_stager    sO    	JJt 	 	 	DD	 s    
))c                   	
 t          | d           t                      	t          | d           t          j                    r	 t          | d           t	          j        t          j        d                    }t          | d           d |D             }g }t          |          D ]t\  }
d
vrkt          | d	           t          	                    

                    d
                    }|r,|                                ||<   |                    |           u|rL	 t          | d           t          |           n*# t          $ r t                              d           Y nw xY wt          | d           |D ](
t#          

                    d          	          
d<   )t          | d           d |D             }t$          5  t'          j                    D ]!

                    d	          |
j        <   "	 d d d            n# 1 swxY w Y   t          | d           t-          |                                d d          }d |D             }d |D             }|D ]


                    d          sd
d<   t          | d           t/          |           |S # t          $ r t                              d           Y nw xY wt          | d           g }t1          j        d          D ]}|j                            d           r	 t          	                    |j                  

r|                    
           U# t          $ r t                              d!|           Y |w xY wt          | d"           t'          j                    D ]2
t;          
fd#|D                       r|                    
           3t          | d$           |                    d% d           	fd&|D             }d' |D             }|D ]


                    d          sd
d<   t          | d           t/          |           |S )(Nzall_sessions.active_streamszall_sessions.index_existszall_sessions.read_indexrN   rO   zall_sessions.prune_indexc                 V    g | ]&}t          |                    d                     $|'S r8   )r0   r:   r[   s     r'   rW   z all_sessions.<locals>.<listcomp>  sB       &quu\':':;;  r?   r   zall_sessions.backfill_loadr)   zall_sessions.backfill_writer_   z*Failed to persist last_message_at backfillzall_sessions.mark_streamingr   r1  zall_sessions.overlay_lockc                      i | ]}|d          |S r8   rY   r[   s     r'   r\   z all_sessions.<locals>.<dictcomp>  s    ;;;<!;;;r?   Tr4  r   zall_sessions.sort_filterc                 L    |                      dd          t          |           fS Nr   F)r:   r   rC   s    r'   rE   zall_sessions.<locals>.<lambda>  s#    quuXu?U?UWnopWqWq>r r?   rF   c                     g | ]w}|                     d d          dk    rY|                     dd          dk    r?|                     d          s*|                     d          s|                     d          u|xS )r   r   r   r   r   r0  r   r9   r[   s     r'   rW   z all_sessions.<locals>.<listcomp>)  s       Agz**j88EE/1--22011 3899 3 o.. 3 222r?   c                 0    g | ]}t          |          |S rY   r  r[   s     r'   rW   z all_sessions.<locals>.<listcomp>0  s&    MMMA/I!/L/LMaMMMr?   r   defaultzall_sessions.lineage_metadataz7Failed to load session index, falling back to full scanzall_sessions.full_scanr4   r5   r6   zall_sessions.full_scan_overlayc              3   8   K   | ]}j         |j         k    V  d S r   r8   )r<   xrD   s     r'   r"  zall_sessions.<locals>.<genexpr>G  s,      99q|q|+999999r?   z"all_sessions.full_scan_sort_filterc                 B    t          | dd          t          |           fS r  )r   r   rC   s    r'   rE   zall_sessions.<locals>.<lambda>I  s!    GAx779PQR9S9ST r?   c                     g | ][}|j         d k    r7t          |j                  dk    r|j        s|j        st          |dd          D|                    d          \S )r   r   r   NTr  )r   r   r   r   r   r   rZ   )r<   rD   r   s     r'   rW   z all_sessions.<locals>.<listcomp>M  s       WX	:
OOq  " !& ! ?D11 ! ii@QiRR   r?   c                 0    g | ]}t          |          |S rY   r  r[   s     r'   rW   z all_sessions.<locals>.<listcomp>T  s&    EEEA'A!'D'DEaEEEr?   )r  r   r   r.   rm   ru   rv   rx   rh   ri   r:   rZ   rj   ry   r$   r    r!   r   r	   r   rk   r)   sortedr  r   r   r"   rR   rS   r$  rl   )r  index
backfilledr   full	index_mapresultoutr&   r   rD   s            @@r'   all_sessionsr    su   3444*,,1222 "" DTC	T7888J1;WMMMNNE8999    E J!%(( 0 01$A--&BCCC"<<l(;(;<<D 0#'<<>>a"))$/// OO&CDDD(<<<<<  O O OLL!MNNNNNO;<<<  $9EE,--%% %.!!
 9:::;;U;;;I  !**  A./ii(,*; /8 / /Ial++               8999I,,..4r4r  }A  B  B  BF    F NMMMMF  - -uuY'' -#,AiL=>>>,V444M 	T 	T 	TLLRSSSSS	T .///
Ch'' > >6S!!+8	>QV$$A#**Q--- 	> 	> 	>LL91=====	>6777_ I I9999S99999H3::a===:;;;HHTT^bHccc   \_   F FEEEEF % %uuY 	%$AiL5666$V,,,Mso   CJ& " E J& $E*'J& )E**A!J& 6HJ& HJ& HBJ& &$KK6M%M32M3r   fallbackc                 4   | D ]}|                     d          dk    ry|                     dd          }t          |t                    rd                    d |D                       }t	          |                                          }|r|dd         c S |S )	z3Derive a session title from the first user message.r   r.  rD  r   rC  c              3      K   | ]H}t          |t                    |                    d           dk    0|                    dd          V  IdS )typer   r   N)r   r   r:   rT   s     r'   r"  ztitle_from.<locals>.<genexpr>c  sZ      mm1Z4=P=PmUVUZUZ[aUbUbflUlUlQUU62..UlUlUlUlmmr?   N@   )r:   r   r   r   r   r   )r   r  mr   r   s        r'   
title_fromr  ]  s     ! !55==F""i$$A!T"" nHHmmmmmmmq66<<>>D !CRCy   Or?   projectsc                    d | D             }|sdS i }t          j                    r	 t          j        t          j        d                    }d |D             }|D ]L}|                    d          }||v r1|                    d          r|                    ||d                    Mn*# t          $ r t          	                    d           Y nw xY wd}|D ]2}|                    |                    d          d	          }	|	|d<   d
}3|S )u  Tag any legacy untagged projects (`profile` missing) with a sensible default.

    Strategy:
      1. For each untagged project, look at the sessions assigned to it via
         the session index. If any session carries a profile, take that
         profile.  Most installs are single-profile so this picks up the
         right answer for everyone.
      2. Otherwise default to 'default'.

    Returns True if any project was mutated. Safe to call repeatedly — once
    every project is tagged, this is a no-op. Runs at most once per process
    (cached via the module-level _projects_migrated flag) but the result is
    persisted so it's a one-time write.
    c                 <    g | ]}|                     d           |S )r   r9   rT   s     r'   rW   z8_backfill_project_profiles_if_needed.<locals>.<listcomp>  s)    <<<a155+;+;<<<<r?   FrN   rO   c                 H    h | ]}|                     d           |d           S )r   r9   rT   s     r'   r>   z7_backfill_project_profiles_if_needed.<locals>.<setcomp>  s-    UUU|ATATUAlOUUUr?   r   r   z9Failed to read session index for project profile backfillr  T)
r   r.   rm   ru   rv   r:   
setdefaultr$   r    r!   )
r  untaggedsession_profile_by_projectr{   untagged_idsr=   pidmutatedr&   inferreds
             r'   $_backfill_project_profiles_if_neededr  p  sZ    =<8<<<H u 24 "" 
V		Vj!3!=w!O!O!OPPGUUXUUUL M MeeL)),&&155+;+;&.99#q|LLL	M
  	V 	V 	VLLTUUUUU	V G  -11!%%2E2EyQQ)Ns   BB* *$CC)_migrater  c                 D   t          j                    sg S 	 t          j        t          j        d                    }n# t
          $ r g cY S w xY w| rt          st          5  t          rR	 t          j        t          j        d                    cddd           S # t
          $ r |cY cddd           S w xY wt          |          r=	 t          |           dan,# t
          $ r t                              d           Y nw xY wdaddd           n# 1 swxY w Y   |S )a  Load project list from disk. Returns list of project dicts.

    On first call, runs a one-time migration to back-fill the `profile` field
    on legacy untagged projects (#1614). Disable via `_migrate=False` for
    callsites that want the raw on-disk shape (test fixtures, e.g.).
    rN   rO   NTz*Failed to persist project profile backfill)r   r.   rm   ru   rv   r$   _projects_migrated_PROJECTS_MIGRATION_LOCKr  save_projectsr    r!   )r  r  s     r'   load_projectsr    s    !! 	:m5wGGGHH   			 ** *% 	* 	*! 
$$:m&=w&O&O&OPP	* 	* 	* 	* 	* 	* 	* 	* ! $ $ $#OO	* 	* 	* 	* 	* 	* 	* 	*$3H== 	*O!(+++)-&&  O O OLL!MNNNNNO
 &*"-	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	*. Osc   '? AA!D*&BB8)D7B88DCD$DDDDDDc                 \    t          j        t          j        | dd          d           dS )zWrite project list to disk.FrI   rJ   rN   rO   N)r   
write_textrm   rn   )r  s    r'   r  r    s0    TZuQOOOZabbbbbbr?   z	Cron Jobsc            	         ddl m} m}  |             pd}t          5  t	                      }|D ]}|                    d          t          k    r!|                    d          }||k    r|d         c cddd           S  ||pd          r! ||          r|d         c cddd           S |D ]_}|                    d          t          k    r?|                    d          s*||d<   t          |           |d         c cddd           S `t          j	                    j
        dd         }|                    |t          d	|t          j                    d
           t          |           |cddd           S # 1 swxY w Y   dS )u   Return the project_id of the system "Cron Jobs" project for the active profile.

    Each profile gets its own "Cron Jobs" project so cron-spawned sessions in
    profile A don't surface under the cron chip of profile B (#1614). Lookup
    keys on (name, profile) — a legacy untagged "Cron Jobs" project (no
    `profile` field) is treated as belonging to whichever profile first calls
    this in a given install, then re-tagged.

    Thread-safe and idempotent.  Returns a 12-char hex project_id string.
    r   )ry  _is_root_profiler  r"   r   r   Nr   z#6366f1)r   r"   colorr   r   )r=  ry  r  _CRON_PROJECT_LOCKr  r:   CRON_PROJECT_NAMEr  r   r   r   rj   r   )ry  r  activer  r&   row_profiler   s          r'   ensure_cron_projectr    sg    GFFFFFFF$$&&3)F	   ??
  	' 	'AuuV}} 111%%	**Kf$$&&          8y99 '>N>Nv>V>V '&&         	' 	'AuuV}} 111!%%	:J:J1%)h'''&&'       * Z\\%crc*
$%)++
 
 	 	 	 	h=                 s&   AE>?!E>-AE>A"E>>FFr   c                 ^    |dk    rdS t          | pd          }|                    d          S )z4Return True if a session originates from a cron job.r  Tr   r  )r   rR   )r)   r   r   s      r'   is_cron_sessionr    s6    Vt
jB

C>>'"""r?   unknownr   r   c                 z    t          | |t                      ||||||	  	        }|                    d           |S )zCreate a new WebUI session populated with CLI/agent messages.

    Preserve parent_session_id from state.db so imported continuation segments
    keep their lineage in the WebUI store and sidebar instead of reappearing as
    detached orphan chats.
    )	r)   r   r   r   r   r   r   rB   r   F)r  )rh   r   r  )	r)   r   r   r   r   r   rB   r   rD   s	            r'   import_cli_sessionr    sT      	$&&+
	 
	 
	A FFEF"""Hr?   claude_codezClaude Code   i   i  i@ c                      t          j        d          } | r!t          |                                           S t          j        d          rdS t          j                    dz  dz  S )zOResolve the Claude Code projects directory without touching real home in tests. HERMES_WEBUI_CLAUDE_PROJECTS_DIRHERMES_WEBUI_TEST_STATE_DIRNz.clauder  )rb   r  r   r   home)overrides    r'   !_default_claude_code_projects_dirr  +  s`    y;<<H +H~~((***	y.// t9;;"Z//r?   r   c                     t          j        t          |                                                                                               d                                                    d d         }t           d| S )NrN      r5   )hashlibsha256r   r   r   r   	hexdigestCLAUDE_CODE_SOURCE)r   digests     r'   _claude_code_session_idr  5  si    ^C 1 1 9 9 ; ;<<CCGLLMMWWYYZ][]Z]^F ++6+++r?   c                    | d S t          | t          t          f          rt          |           S t          |                                           }|sd S 	 t          |          S # t
          $ r Y nw xY w	 t          j                            |                    dd                    	                                S # t          $ r Y d S w xY w)NZ+00:00)r   r   r   r   r   r   datetimefromisoformatrt   r   r$   valuer   s     r'   _parse_claude_code_timestampr  :  s    }t%#u&& U||u::D tT{{    ..t||C/J/JKKUUWWW   tts%   A% %
A21A26AB; ;
C	C	c                    | dS t          | t                    r| d t                   S t          | t                    rg }d}| D ]}d}t          |t                    r|}nAt          |t                    r,|                    d          p|                    d          pd}|s`t          |          }t          |z
  }|dk    r n6|                    |d |                    |t          |d                   z  }d                    |          S t          | t                    r7t          |                     d          p|                     d                    S t          |           d t                   S )Nr   r   r   rD  r   
)
r   r   CLAUDE_CODE_MAX_CONTENT_CHARSr   r   r:   rj   r   r   _extract_claude_code_text)rD  partsuseditemr   	remainings         r'   r  r  L  s{   r'3 755566'4     	# 	#DD$$$ ED$'' Exx''D488I+>+>D" t99D5<IA~~LLjyj)***Cb	NN"DDyy'4   X(V)<)<)VI@V@VWWWw<<66677r?   )max_messagesr  c                   g }d }d }d }	 |                      ddd          5 }|D ]u}t          |          |k    r n^|                                }|s0	 t          j        |          }n# t
          $ r Y Rw xY wt          |t                    sl|s|                    d          p|                    d          }	t          |	t                    rC|	                                r/d
                    |	                                          d d         }t          |                    d	          t                    r|                    d	          nd }
|
@t          |                    d
          t                    r|                    d
          n|g}
|
D ] }t          |          |k    r nt          |t                    s/t          |                    d
          t                    r|                    d
          n|}t          |                    d          p@|                    d          p+|                    d          p|                    d          pd                                                                          }|dk    rd}|dvrt          d|v r|                    d          n|                    d                    }|                                sTt          |                    d          p>|                    d          p)|                    d          p|                    d                    }|(||nt!          ||          }||nt#          ||          }||d}|||d<   |                    |           wd d d            n# 1 swxY w Y   n# t
          $ r	 g d d d fcY S w xY w||||fS )Nr   rN   rt   )rP   errorssummaryr   rC  P   r   r   r   r  r   humanr.  >   r   r.  systemrG  rD  r   r   )r   rD  )ro   r   r   rm   ru   r$   r   r   r:   r   r   rI  r   r   r  r  minr2  rj   )r   r  r   summary_titlefirst_tslast_tsfhliner   r  recordsrecordmsgr   rD  r   r  s                    r'   _parse_claude_code_jsonlr  h  s:   HMHG1$YYsWYY?? .	*2 -* -*x==L00Ezz|| *T**CC    H!#t,, $ G!ggi00DCGGG4D4DG!'3// GGMMOO G(+(A(A#2#(F1;CGGJ<O<OQU1V1V`#''*---\`?5?	@R@RTX5Y5Ybswwy111_bcG% * *F8}}44%fd33 ! 3=fjj>S>SUY3Z3Zf&**Y///`fCswwvp&**V2D2DpPVp[^[b[bci[j[jpnpqqwwyy  B  BDw%#JJJ 7i[^N^N^	8J8J8JdjdndnoxdydyzzG"==?? ! 5,, 1!::k22177;//1 77<00	 B ~)1)922s8R?P?P(/""S"=M=M$(W==D~,.[)OOD))))].	* .	* .	* .	* .	* .	* .	* .	* .	* .	* .	* .	* .	* .	* .	*^  $ $ $4t####$]Hg55sY   N4 3N(A+*N(+
A85N(7A88L$N(N4 (N,,N4 /N,0N4 4OO	max_filesmax_file_bytesprojects_dirr  r	  c             #   |  K   | !t          |                                           nt                      }|d S 	 |                                rd S |                    d          }|                                r|                                sd S d}t          |                                d           D ]}||k    r d S 	 |                                s|                                s5t          |                                d           D ]}||k    r  d S |                                s1|	                                r|j
                                        dk    rR	 |                                j        |k    rqn# t          $ r Y ~w xY w|dz  }|V  # t          $ r Y w xY wd S # t          $ r Y d S w xY w)	NF)strictr   c                     | j         S r   r"   r&   s    r'   rE   z/_iter_claude_code_jsonl_files.<locals>.<lambda>  s     r?   )rG   c                     | j         S r   r  r  s    r'   rE   z/_iter_claude_code_jsonl_files.<locals>.<lambda>  s     r?   z.jsonlr   )r   r   r  
is_symlinkr   r.   is_dirr  iterdiris_filesuffixr   r   st_sizer#   )r
  r  r	  rootyieldedproject_dirr   s          r'   _iter_claude_code_jsonl_filesr    s%     .:.F4((***LmLoLoD|?? 	F||5|)){{}} 	DKKMM 	F!$,,..6F6FGGG 	 	K)##))++ ;3E3E3G3G ";#6#6#8#8>N>NOOO  D)++(( ! !$+BSBSBUBUYaBaBa !99;;.??$ @" ! ! ! !qLGJJJJ    %	 	(    s~   F- >F- /F- (F)F- *-FF- AF!F >F 
F
FFFF- 
F(%F- 'F((F- -
F;:F;r   r  c                     |r|S | D ]r}|                     d          dk    rWd                    t          |                     d          pd                                                    }|r|d d         c S sdS )Nr   r.  rC  rD  r   r  zClaude Code Session)r:   r   r   rI  )r   r  r  r   s       r'   _claude_code_titler    s      ! !776??f$$88C	 2 2 8b99??AABBD !CRCy     r?   c          	      x   g }t          | ||          pg D ]	}t          |          \  }}}}|st          |          }	|                    i d|	dt	          ||          dt          t                                dddt          |          d|p|p|                                j	        d	|p|p|                                j	        d
|p|p|                                j	        dddddddddt          dt          dddt          ddddi           |                    d d           |S )aS  Read Claude Code JSONL sessions as read-only external-agent rows.

    The bridge is additive and defensive: it skips symlinks, oversized files,
    malformed lines, and per-file errors rather than crashing WebUI session
    listing. Tests pass ``projects_dir`` fixtures so Michael's real ~/.claude is
    never read during test runs.
    r  r)   r   r   r   zclaude-coder   r   rB   r   r   Fr   r   Nr   r   r   r   external_agentr   r   T	read_onlyc                 Z    |                      d          p|                      d          pdS )Nr   rB   r   r9   rC   s    r'   rE   z*get_claude_code_sessions.<locals>.<lambda>  s*    &7 8 8 TAEE,<O<O TST r?   rF   )r  r  r  rj   r  r   r   r   r   r   r  CLAUDE_CODE_SOURCE_LABELrl   )
r
  r  r	  r\  r   r   r  r  r   r   s
             r'   get_claude_code_sessionsr"    s    H-li`nooousu  5Md5S5S2-7 	%d++ 
#
'-@@
 /1122
 ]	

 S]]
 (EgE1E
 'EXE1E
 wJ(Jdiikk6J
 e
 
 $
 t
 ,
 ,
 .
  4!
" d#
$ %
 
 	 	 	 	( MMTT^bMcccOr?   c                     t          | pd          } |                     t           d          sg S t          |          pg D ]-}t	          |          | k    rt          |          \  }}}}|c S g S )z<Return messages for one read-only Claude Code JSONL session.r   r5   )r   rR   r  r  r  r  )r   r
  r   r   _summary_title	_first_ts_last_tss          r'    get_claude_code_session_messagesr'    s    
ciR..C>>/22233 	-l;;Ar  "4((C//8PQU8V8V5.)XIr?   c            
      $   ddl } g }	 |                    t                                 n,# t          $ r t                              dd           Y nw xY w	 ddlm} t           |                      	                                
                                }ng# t          $ rZ t           | j        dt          t          dz                                	                                
                                }Y nw xY w|d	z  }|                                s|S 	 dd
lm}  |            }n# t           $ r d}Y nw xY wdgfd}	 t#          |t$          t          d          D ]}|d         }	|d         p|d         }
|}|d         pd}|d         }|s|dk    r|	                    d          r|	                    d          }t+          |          dk    r|d         }	 |dz  dz  }|                                rvddl}|                    |                                          }|                    dg           D ]4}|                    d          |k    r|                    d          p|} n5n# t          $ r Y nw xY w	 t4                              |	          }|rt9          |dd          r|j        }n# t          $ r Y nw xY w|p|                                 d}|                    i d|	d|dt          t?                                d|d         pdd|d         p	|d          pdd!|d         d"|
d#d$d%d$d&tA          |	|          r
 |            ndd'|d(|d)|                    d)          d*|                    d*          d+|                    d+          p|                    d,          d-|                    d-          d.|                    d.          i d/|                    d/          d0|                    d0          d1|                    d1          d2|                    d2          d3|                    d3          d4|                    d4          d5|                    d5          d6|                    d6          d7|                    d7          d8|                    d8          d |                    d           d9|                    d:          d;|                    d;          d<|                    d<          d=|                    d=          d>d           nL# t          $ r?}ddl!}|"                    tF                    $                    d?||           g cY d}~S d}~ww xY w|S )@a  Read CLI sessions from the agent's SQLite store and return them as
    dicts in a format the WebUI sidebar can render alongside local sessions.

    Returns empty list if the SQLite DB is missing or any error occurs -- the
    bridge is purely additive and never crashes the WebUI.
    r   NzClaude Code session scan failedT)exc_infor  r<  r  r  rx  c                  F     d         t                       d<    d         S )Nr   )r  )_cron_pid_caches   r'   	_cron_pidz#get_cli_sessions.<locals>._cron_pid/  s)    1%!4!6!6OAq!!r?   )limitlogexclude_sourcesidlast_activity
started_atr  clir   r  r  r5      r   z	jobs.jsonjobsr"   z Sessionr)   r   r   r   actual_message_countr   rB   r   Fr   r   r   r   r   user_idchat_idorigin_chat_id	chat_type	thread_idsession_keyplatformr   r   r   parent_titleparent_sourcerelationship_type_parent_lineage_root_id
end_reasonr/  actual_user_message_count_lineage_root_id_lineage_tip_id_compression_segment_countr   uD   get_cli_sessions() failed — check state.db schema or path (%s): %s)%rb   extendr"  r$   r    r!   r=  r  r   r   r   r  r   r   r.   ry  r>  r   CLI_VISIBLE_SESSION_LIMITrR   rI  r   rm   ru   rv   r:   rh   r+  r   r   rj   r   r  logging	getLoggerr5  ra  )rb   cli_sessionsr  r  db_pathry  _cli_profiler,  rowr   raw_tsr   _source_titler  _job_id
_jobs_path_json
_jobs_data_j_webui_meta_display_title_cli_err_loggingr+  s                           @r'   get_cli_sessionsr[    s4    IIILG4667777 G G G6FFFFFGc777777113344??AAIIKK c c c929]Cy8H4I4IJJKKVVXX``bbc J&G>> 888888..00    fO" " " " "
V5+ 	
 
 
 L	 L	C d)C)>S->F #G(m,uG\F g//CNN74K4K/ 		#u::??#AhG
%06%9K%G
%,,.. *0000).Z5I5I5K5K)L)LJ&0nnVR&@&@ * *#%66$<<7#:#:-/VVF^^-EvF$)E $; %   %88== /7;#F#F /(.F   #C'--//'C'C'CN "!c"!"! S!3!5!566"! W-	"!
  _!5!Y=S9T!YXY"! c,/"! f"! %"! E"! _S'-J-JTiikkkPT"! 7"! g"! cggl33"! 3779--"! 3779--J9I1J1J"!  SWW[11!"!" SWW[11#"! "!$ sww}55%"!& CGGJ//'"!( !#''*:";";)"!*  7 7+"!, $SWW-@%A%A-"!.  7 7/"!0  !9!91"!2 $SWW-@%A%A3"!4 *3773L+M+M5"!6 cggl337"!8 '0F(G(G9"!: %cgg.I&J&J;"!< #CGG,>$?$?="!> "377+<#=#=?"!@ -cgg6R.S.SA"!B !$C"! "! " " " "UL	Z     	#"""8$$,,RX	
 	
 	
 						 s   !+ &AAAB A!C>=C>D. .D=<D=	BU #BI65U 6
J U JU 4J<;U <
K	U K		I:U 
V4VVVc                     t          | t                    s| S |                                 }|sd S 	 t          j        |          S # t
          $ r | cY S w xY wr   )r   r   r   rm   ru   r$   r  s     r'   _json_loads_if_stringr]    sl    eS!! ;;==D tz$   s   A AAc           
      	   ddl }t          | pd                              t           d          rt	          |           S 	 ddl}n# t          $ r g cY S w xY w	 ddlm} t           |                      
                                                                }ng# t          $ rZ t           |j        dt          t          dz                                
                                                                }Y nw xY w|dz  }|                                sg S 	 t!          |                    t          |                              5 }|j        |_        |                                }|                    d	           d
 |                                D             h d}|                              sg cddd           S g d}	g dfd|	D             z   }
|                    d           d |                                D             }t          |           g}h d                    |          rp|                    d| f           i }|                                }|r@t3          |          |t          |d                   <   t          |d                   }|h}t5          d          D ]}|                    |          }|r|                    d          nd}|r||v r n|                    d|f           |                                }|s nt3          |          }||t          |d                   <   t9          ||          s nT|                    dt          |d                              t          |d                   }|                    |           d                    d |D                       }|                    dd                    |
           d| d|           g }|                                D ]}|d         |d         |d         d}|	D ]>}||                                 vr||         }|dv r&|d v rtC          |          }|||<   ?|                    d          d!k    r5|                    d"          r |                    d#          s|d"         |d#<   |"                    |           	 ddd           n# 1 swxY w Y   n# t          $ r g cY S w xY w|S )$a  Read messages for a single CLI/external-agent session.

    Preserve tool-call/result and reasoning metadata from the agent state.db so
    CLI-origin transcripts render with the same tool cards as WebUI-native
    sessions. When the requested session is the tip of a compression/CLI-close
    continuation chain, return the stitched full transcript across all segments
    in chronological order. Returns empty list on any error.
    r   Nr   r5   r  r<  r  r  zPRAGMA table_info(messages)c                 8    h | ]}t          |d                    S r  r   r<   rN  s     r'   r>   z+get_cli_session_messages.<locals>.<setcomp>  s$    DDDcS[))DDDr?   >   r   rD  r   )tool_call_idr   	tool_name	reasoningreasoning_detailscodex_reasoning_itemsreasoning_contentcodex_message_items)r   rD  r   c                     g | ]}|v |	S rY   rY   )r<   r   	availables     r'   rW   z,get_cli_session_messages.<locals>.<listcomp>  s%    :a:a:aRSW`R`R`1R`R`R`r?   zPRAGMA table_info(sessions)c                 8    h | ]}t          |d                    S r  r`  ra  s     r'   r>   z+get_cli_session_messages.<locals>.<setcomp>  s$    GGGCF,,GGGr?   >   r  rB  r2  r   z
                    SELECT id, source, started_at, parent_session_id, ended_at, end_reason
                    FROM sessions
                    WHERE id = ?
                    r0  r   r   z
                            SELECT id, source, started_at, parent_session_id, ended_at, end_reason
                            FROM sessions
                            WHERE id = ?
                            z, c              3      K   | ]}d V  dS )?NrY   )r<   r5   s     r'   r"  z+get_cli_session_messages.<locals>.<genexpr>  s"      $@$@QS$@$@$@$@$@$@r?   z
                SELECT zP, session_id
                FROM messages
                WHERE session_id IN (z=)
                ORDER BY timestamp ASC, id ASC
            r   rD  r   )Nr   >   r   re  rh  rf  r   rc  r"   )#rb   r   rR   r  r'  sqlite3r>  r=  r  r   r   r   r$   r  r   r.   r   connectRowrow_factorycursorexecutefetchallr(  fetchoner   ranger:   _is_continuation_sessioninsertaddr   r-   r]  rj   )r   rb   rn  r  r  rL  conncurrequiredoptionalselectedsession_colssession_chain
rows_by_idrN  
current_idseenr5   current	parent_id
parent_rowparent_dictplaceholdersmsgsr  colr  rj  s                              @r'   get_cli_session_messagesr    s    III
39"~~  $6!9!9!9:: 5/444   			c777777113344??AAIIKK c c c929]Cy8H4I4IJJKKVVXX``bbcJ&G>> 	ZW__S\\2233 W	!t&{D++--CKK5666DDS\\^^DDDI777H$$Y// W	! W	! W	! W	! W	! W	! W	! W	!	 	 	H 877:a:a:a:ah:a:a:aaHKK5666GGGGGL XXJMJJJSST`aa %-
 F    
llnn -15cJs3t9~~.!$SYJ&<D"2YY - -",.."<"<HO$YGKK0C$D$D$DUY	( "I,=,=!E 
 'L   &)\\^^
) "!E&*:&6&6<G
3z$'7#8#897WMM "!E%,,QJt4D0E0EFFF%(D)9%:%:
,,,,99$@$@-$@$@$@@@LKK 		(++  '3  
       D||~~ ! !K"9~!$[!1 
 $ % %C#((**,, HE
** qqq 5e < <$CHH776??f,,1E1E,cggV\oo,"%k"2CKC    #!MW	! W	! W	! W	! W	! W	! W	! W	! W	! W	! W	! W	! W	! W	! W	!p    			Ksj   A AAAB A!D ?D /S( A.S<S( 	LSS( S  S( #S $S( (S76S7r   sincec           
         ddl }ddl}ddl}	 ddlm} t           |                                                                                      }ng# t          $ rZ t           |j	        dt          t          dz                                                                                                }Y nw xY w|dz  }|                                sdS 	 |                    t          |                    5 }|j        |_        |                                }	|	                    d| f           |	                                }
ddd           n# 1 swxY w Y   n# t          $ r Y dS w xY wd}d}d}|
D ]}|d	         pd
                                                                }|d         }||	 t+          |t,          t.          f          rt/          |          }nM|j                            t          |                              dd                                                    }||k    rn# t          $ r Y nw xY w|dk    r|r|sn|r	|r|dz  }d}d}|dk    r|rd}|r|r|dz  }|S )a  Count conversation rounds for a session from state.db.

    A "round" = one user message + one agent reply.  Consecutive user
    messages are merged into a single round so that multi-part questions
    don't inflate the count.

    Parameters
    ----------
    sid : str
        Gateway session ID (e.g. ``20260430_151231_7209a0``).
    since : float | None
        Unix timestamp.  If provided, only messages **after** this
        timestamp are counted.

    Returns
    -------
    int
        Number of complete conversation rounds.
    r   Nr  r<  r  r  zPSELECT role, timestamp FROM messages WHERE session_id = ? ORDER BY timestamp ASCFr   r   r   r  r  r.  r   TrG  )rb   rn  r  r=  r  r   r   r   r$   r  r   r   r.   ro  rp  rq  rr  rs  rt  r   r   r   r   r   r  rt   r   )r   r  rb   rn  r  r  r  rL  rz  r{  rowsrounds	seen_userseen_agent_after_userrN  r   ts_rawts_vals                     r'   count_conversation_roundsr    sC   ( !           c777777113344??AAIIKK c c c929]Cy8H4I4IJJKKVVXX``bbcJ&G>> q
__S\\** 	"d&{D++--CKKb   <<>>D	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	"    qq FI! - -F!r((**0022[! !3fsEl33 ""6]]FF &.<<F++C:: ikk  U?? #    6>> .!6 . .4 .!(-%II[   -(,%  * !Ms]   AA A!B43B4"E 5AEE EE EE 
E'&E'/A?H00
H=<H=
   c           
      :   ddl }	 ddl}n# t          $ r Y dS w xY w	 ddlm} t           |                                                                                      }ng# t          $ rZ t           |j	        dt          t          dz                                                                                                }Y nw xY w|dz  }|                                sdS 	 t          |                    t          |                              5 }|                                }|                    d| f           |                    d	| f           |                                 |j        dk    cddd           S # 1 swxY w Y   dS # t          $ r Y dS w xY w)
z{Delete a CLI session from state.db (messages + session row).
    Returns True if deleted, False if not found or error.
    r   NFr  r<  r  r  z)DELETE FROM messages WHERE session_id = ?z!DELETE FROM sessions WHERE id = ?)rb   rn  r>  r=  r  r   r   r   r$   r  r   r   r.   r   ro  rr  rs  commitrowcount)r   rb   rn  r  r  rL  rz  r{  s           r'   delete_cli_sessionr  l  s    III   uuc777777113344??AAIIKK c c c929]Cy8H4I4IJJKKVVXX``bbcJ&G>> uW__S\\2233 	$t++--CKKCcVLLLKK;cVDDDKKMMM<!#	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$    uusV    
AA A!CC"/F A!E?2F ?FF FF 
FF)r   Nr   )r   )F)NNNNNN)r   )r  NNNN)j__doc__collectionsr  r  rm   rI  rb   rd   r   r   
contextlibr   pathlibr   
api.configconfig_cfgr   r   r   r   r	   r
   r   r   r   r   r   r   r   api.workspacer   api.agent_sessionsr   r   rJ  r5  r    rH  r   RLockrg   r(   r   r   r0   ry   r   r   r   r   r   r   r   r   r   rh   r@  rW  r_  rl  rv  r  r   r  r  r   r  r  r  r  Lockr  r  r  r  r  r  r  r  r  r  r  r!  CLAUDE_CODE_MAX_FILESCLAUDE_CODE_MAX_FILE_BYTES!CLAUDE_CODE_MAX_MESSAGES_PER_FILEr  r  r  r  r  r   tupler   r  r  r  r"  r'  r[  r]  r  r  CONVERSATION_ROUND_THRESHOLDr  rY   r?   r'   <module>r     s+   A A         				                                                     
 - , , , , , ` ` ` ` ` ` ` `		8	$	$    $IO%%    * C     &h+ h+ h+ h+V# # #
L L Lq q q  	 	 	8 8 8% % %P   &  ,n
 n
 n
 n
 n
 n
 n
 n
`	
O$ 
O 
O 
O 
O  B
 B B B 
B B B Bl ') #Rd R R R Rj) ) ) )V5 5 5 5n7 7 7 7 7 7$t $ $ $ $*tDz *d * * * *C D    i i i iX
 
3 
 
 
 
 *9>++  &4 &D & & & &R '+ & & &t &t & & & &Pc c c c
   #Y^%% ,S , , , ,^# # # # # # # #   	   B # (  - $( ! ' 04$; 0 0 0 0,$ ,3 , , , ,
  $8# 8 8 8 88 Ab 76 76 764 76# 76fklpqulvx{  C  yC  EJ  MQ  EQ  SX  [_  S_  m_  g` 76 76 76 76t ^s  Lf      s
T0A  X[    FI        F!d !C$J !3 ! ! ! !#Yn  Ga # # #4#:+< #SV #  AD #  fj # # # #L
 
s
T8I 
UY 
 
 
 
I$ I I I IX	 	 	uT u u u upS S3 Sut| Ss S S S Sl  " t      r?   