Ë
    âß²j�,  ã                  óø   — d Z ddlmZ ddlZddlZddlmZmZmZ ddl	m
Z
 ddlmZmZ  ej                  d«      Z ej"                  d«      Z ej"                  d	«      Z ej"                  d
«      Z G d„ d«      Zy)zgSession title mixin for SessionDB: sanitizing, auto/user provenance ranking, and
lineage-aware lookups.é    )ÚannotationsN)ÚAnyÚDictÚOptional)Ú_sanitize_surrogates)Ú_COMPRESSION_CHILD_SQLÚescape_likeÚhermes_statez [\x00-\x08\x0b\x0c\x0e-\x1f\x7f]zB[\u200b-\u200f\u2028-\u202e\u2060-\u2069\ufeff\ufffc\ufff9-\ufffb]z^(.*?) #(\d+)$c                  ó„   — e Zd ZdZedd„«       Zedd„«       Zdd„Zdd„Z	dd„Z
dd„Zdd„Zdd	„Zdd
„Zdd„Zdd„Zdd„Zy)ÚSessionTitlesMixinz<Sanitizing, ranking auto/user titles, lineage-aware lookups.c                ór   — | j                   }|€|| j                     S |j                  t        |«      d«      S )z’Rank a stored title_source. NULL (pre-provenance rows) is indistinguishable from a
        manual ``/title`` of that era, so it ranks as ``user``.r   )Ú_TITLE_SOURCE_RANKÚTITLE_SOURCE_USERÚgetÚstr)ÚclsÚsourceÚranks      ú8/home/ubuntu/.hermes/hermes-agent/hermes_state_titles.pyÚ_title_rankzSessionTitlesMixin._title_rank   s9   € ð ×%Ñ%ˆØ.4¨nˆt�C×)Ñ)Ñ*ÐZÀ$Ç(Á(Ì3ÈvË;ÐXYÓBZÐZó    c           	     óH  — ddl m} | syt        j                  dt        j                  dt        | «      «      «      }t        j                  dd|«      j                  «       }|syt        |«      |j                  kD  r%t        dt        |«      › d|j                  › d	�«      ‚|S )
z¦Strip control/zero-width/bidi chars (and lone surrogates sqlite3 cannot bind),
        collapse whitespace, normalize empty to None. ValueError past MAX_TITLE_LENGTH.r   )Ú	SessionDBNÚ z\s+Ú zTitle too long (z chars, max ú))r
   r   Ú_TITLE_INVISIBLE_REÚsubÚ_TITLE_CONTROL_REr   ÚreÚstripÚlenÚMAX_TITLE_LENGTHÚ
ValueError)Útitler   Úcleaneds      r   Úsanitize_titlez!SessionTitlesMixin.sanitize_title!   s“   € õ 	+ÙØÜ%×)Ñ)¨"Ô.?×.CÑ.CÀBÔH\Ð]bÓHcÓ.dÓeˆÜ—&‘&˜  gÓ.×4Ñ4Ó6ˆÙØÜˆw‹<˜)×4Ñ4Ò4ÜÐ/´°G³¨~¸\È)×JdÑJdÐIeÐefÐgÓhÐhØˆr   c               ó”   — |r|r||k(  ryt        j                  d¬«      }|j                  d|› d�|||f«      j                  «       duS )zàTrue if *ancestor_id* is a compression predecessor of *descendant_id*, via the
        canonical continuation edge ``_COMPRESSION_CHILD_SQL`` (excludes delegate/branch
        children that also carry ``parent_session_id``).FÚchild)Úaa9  
            WITH RECURSIVE ancestors(id) AS (
                SELECT ?
                UNION
                SELECT parent.id
                FROM ancestors a
                JOIN sessions child ON child.id = a.id
                JOIN sessions parent ON parent.id = child.parent_session_id
                WHERE z`
            )
            SELECT 1 FROM ancestors WHERE id = ? AND id != ? LIMIT 1
            N)r   ÚformatÚexecuteÚfetchone)ÚselfÚconnÚancestor_idÚdescendant_idÚedges        r   Ú_is_compression_ancestorz+SessionTitlesMixin._is_compression_ancestor0   sj   € ñ ¡-°;À-Ò3OØÜ%×,Ñ,¨wÔ7ˆØ�|‰|ð !ð �fð ðð   ¨mÐ<ó>÷ ?G¹h»jÐPTðUð 	Ur   c               ó´   ‡ ‡‡‡‡‡— ‰ j                  ‰«      Š‰‰ j                  k(  Š‰s‰ j                  ‰«      ndŠˆˆˆ ˆˆˆfd„}‰ j                  |«      dkD  S )a  Write a title, enforcing provenance precedence. A ``user`` write always lands;
        ``derived``/``llm`` land only when the row is untitled or holds strictly lower
        authority (derived upgrades to llm exactly once, nothing overwrites a user name,
        re-running the titler on an llm row is a no-op). No writer may move a hidden
        canonical Bot Chat off its title. Read and write are one compare-and-swap
        transaction, so a manual ``/title`` racing an in-flight generation is not clobbered.Nc           	     ó¨  •— | j                  d‰f«      j                  «       }|€y|d   xs d‰j                  k(  r+t        |d   «      r‰	‰j                  k7  r‰rt	        d«      ‚y‰s|d   �‰j                  |d   «      ‰k\  ry‰	r¡| j                  d‰	‰f«      j                  «       }|r}|d	   }‰j                  | |‰¬
«      r| j                  d|f«       nP‰	‰j                  k(  r0t        |d   «      r"t        |d   «      r| j                  d|f«       nt	        d‰	› d|› �«      ‚| j                  d‰	‰	r‰nd ‰|d   |d   f«      j                  S )Nz=SELECT title, title_source, hidden FROM sessions WHERE id = ?r   r%   r   Úhiddenu›   This is the bot's canonical Bot Chat â€” its name is its identity, and renaming it would orphan the conversation. To start fresh, create a new bot instead.Útitle_sourcezESELECT id, archived, hidden FROM sessions WHERE title = ? AND id != ?Úid)r0   r1   z-UPDATE sessions SET title = NULL WHERE id = ?ÚarchivedzBUPDATE sessions SET title = NULL, title_source = NULL WHERE id = ?zTitle 'z' is already in use by session zaUPDATE sessions SET title = ?, title_source = ? WHERE id = ? AND title IS ? AND title_source IS ?)r,   r-   ÚCANONICAL_BOT_CHAT_TITLEÚboolr$   r   r3   Úrowcount)
r/   ÚcurrentÚconflictÚconflict_idÚis_userÚnew_rankr.   Ú
session_idr   r%   s
       €€€€€€r   Ú_doz2SessionTitlesMixin._set_session_title.<locals>._doO   s¡  ø€ Ø—l‘lØOÐR\ÐQ^óç‰h‹jð ð ˆØð
 ˜Ñ!Ò' R¨D×,IÑ,IÒIÌdÐSZÐ[cÑSdÔNeØ ×!>Ñ!>Ò>ÙÜ$ð &Qó Rð Rð Ù˜w wÑ/Ð;À×@PÑ@PÐQXÐYgÑQhÓ@iÐmuÒ@uØÙØŸ<™<Ø[Ð^cÐeoÐ]póç‘(“*ð ñ Ø"*¨4¡.�Kð ×4Ñ4°TÀ{ÐblÐ4ÔmØŸ™Ð%TÐWbÐVdÕeð
   4×#@Ñ#@Ò@ÄTÈ(ÐS]ÑJ^ÔE_Ü" 8¨HÑ#5Ô6ØŸ™Ø`Ø(˜Nõô
 )¨7°5°'Ð9XÐYdÐXeÐ)fÓgÐgð —<‘<ØsØ¡%™¨T°:¸wÀwÑ?OÐQXÐYgÑQhÐió÷ ‰hðr   r   )r'   r   r   Ú_execute_write)r.   rB   r%   r   rC   r@   rA   s   ```` @@r   Ú_set_session_titlez%SessionTitlesMixin._set_session_titleD   s\   ý€ ð ×#Ñ# EÓ*ˆØ˜D×2Ñ2Ñ2ˆÙ3:�4×#Ñ# FÔ+Àˆ÷.	ñ .	ð` ×"Ñ" 3Ó'¨!Ñ+Ð+r   c                ó>   — | j                  ||| j                  ¬«      S )z…Set a title on the user's behalf (``user`` provenance). Empty clears it. Raises
        ValueError on conflict or validation failure.©r   )rE   r   )r.   rB   r%   s      r   Úset_session_titlez$SessionTitlesMixin.set_session_title�   s"   € ð ×&Ñ& z°5À×AWÑAWÐ&ÓXÐXr   c               óz   — || j                   | j                  fvrt        d|›�«      ‚| j                  |||¬«      S )zVSet an automatic title; False (untouched) when a higher-authority title holds the row.z invalid automatic title source: rG   )ÚTITLE_SOURCE_DERIVEDÚTITLE_SOURCE_LLMr$   rE   )r.   rB   r%   r   s       r   Úset_auto_titlez!SessionTitlesMixin.set_auto_title†   sF   € à˜$×3Ñ3°T×5JÑ5JÐKÑKÜÐ?À¸zÐJÓKÐKØ×&Ñ& z°5ÀÐ&ÓHÐHr   c                ó:   — | j                  d|f«      }|r|d   S dS )z%Get the title for a session, or None.z'SELECT title FROM sessions WHERE id = ?r%   N©Ú	_read_one©r.   rB   Úrows      r   Úget_session_titlez$SessionTitlesMixin.get_session_titleŒ   s&   € à�n‰nÐFÈÈÓVˆÙ"ˆs�7‰|Ð,¨Ð,r   c                óD   — | j                  d|f«      }|r
|d   �|d   S dS )z?Get the provenance of a session's title, or None when untitled.z5SELECT title, title_source FROM sessions WHERE id = ?r%   Nr7   rN   rP   s      r   Úget_session_title_sourcez+SessionTitlesMixin.get_session_title_source‘   s2   € à�n‰nÐTÐWaÐVcÓdˆÙ&)¨c°'©lÐ.Fˆs�>Ñ"ÐPÈDÐPr   c                óh   — || j                   vrt        d|›�«      ‚| j                  d||f«      dkD  S )z�Overwrite a title's provenance without touching the text (a title copied across a
        compression rotation keeps the original's authority).zinvalid title source: zGUPDATE sessions SET title_source = ? WHERE id = ? AND title IS NOT NULLr   )r   r$   Ú_write_rowcount)r.   rB   r   s      r   Úset_session_title_sourcez+SessionTitlesMixin.set_session_title_source–   sL   € ð ˜×0Ñ0Ñ0ÜÐ5°f°ZÐ@ÓAÐAØ×#Ñ#ØUÐX^Ð`jÐWkó
àñð 	r   c                óR   — | j                  d|f«      }|r| j                  |«      S dS )z?Look up a session by exact title. Returns session dict or None.z«SELECT s.*, COALESCE(sp.prompt, s.system_prompt) AS _system_prompt_resolved FROM sessions s LEFT JOIN system_prompts sp ON sp.hash = s.system_prompt_hash WHERE s.title = ?N)rO   Ú_session_row_dict)r.   r%   rQ   s      r   Úget_session_by_titlez'SessionTitlesMixin.get_session_by_titleŸ   s4   € à�n‰nð à"' ó+ˆñ /2ˆt×%Ñ% cÓ*Ð;°tÐ;r   c                ó´   — | j                  |«      }|�|| j                  k(  r|d   S | j                  dt        |«      › d�f«      }|r|d   d   S |r|d   S dS )zOResolve a title to a session ID, preferring the latest "title #N" continuation.Nr8   zaSELECT id, title, started_at FROM sessions WHERE title LIKE ? ESCAPE '\' ORDER BY started_at DESCú #%r   )rZ   r:   Ú	_read_allÚ_escape_like)r.   r%   ÚexactÚnumbereds       r   Úresolve_session_by_titlez+SessionTitlesMixin.resolve_session_by_title§   s{   € à×)Ñ)¨%Ó0ˆð Ð ¨$×*GÑ*GÒ!GØ˜‘;Ðà—>‘>ðFä˜UÓ#Ð$ CÐ(Ð*ó,ˆñ %-ˆx˜‰{˜4Ñ ÐRÁ%°5¸±;ÐRÈTÐRr   c                ó6  — t         j                  |«      }|r|j                  d«      n|}| j                  d|t	        |«      › d�f«      }|s|S d„ |D «       D �cg c]  }|sŒt        |j                  d«      «      ‘Œ! }}|› dt        dg|¢«      dz   › �S c c}w )z†Next title in a lineage ("my session" -> "my session #2"): strip any " #N" suffix,
        then increment the highest existing number.é   zESELECT title FROM sessions WHERE title = ? OR title LIKE ? ESCAPE '\'r\   c              3  óL   K  — | ]  }t         j                  |d    «      –— Œ y­w)r%   N)Ú_NUMBERED_TITLE_REÚmatch)Ú.0rQ   s     r   ú	<genexpr>z?SessionTitlesMixin.get_next_title_in_lineage.<locals>.<genexpr>Å   s!   è ø€ Ò,dÐX[Ô-?×-EÑ-EÀcÈ'Ál×-SÑ,dùs   ‚"$é   z #)re   rf   Úgroupr]   r^   ÚintÚmax)r.   Ú
base_titlerf   ÚbaseÚrowsÚmÚnumberss          r   Úget_next_title_in_lineagez,SessionTitlesMixin.get_next_title_in_lineageº   s¥   € ô #×(Ñ(¨Ó4ˆÙ!&ˆu�{‰{˜1Œ~¨JˆØ�~‰~ØTØ”l 4Ó(Ð)¨Ð-Ð.ó0ˆñ ØˆKá,dÐ_cÔ,dÖj qÒhi”3�q—w‘w˜q“z•?ÐjˆÐjØ��rœ#˜q˜m 7˜mÓ,¨qÑ0Ð1Ð2Ð2ùò ks   ÁBÁ"BN)r   úOptional[str]Úreturnrk   )r%   rs   rt   rs   )r0   r   r1   r   rt   r;   )rB   r   r%   r   r   r   rt   r;   )rB   r   r%   r   rt   r;   )rB   r   rt   rs   )rB   r   r   r   rt   r;   )r%   r   rt   zOptional[Dict[str, Any]])r%   r   rt   rs   )rm   r   rt   r   )Ú__name__Ú
__module__Ú__qualname__Ú__doc__Úclassmethodr   Ústaticmethodr'   r3   rE   rH   rL   rR   rT   rW   rZ   ra   rr   © r   r   r   r      se   „ ÙFàò[ó ð[ð òó ðóUó(;,ózYó
Ió-ó
Qó
ó<óSô&3r   r   )rx   Ú
__future__r   Úloggingr    Útypingr   r   r   Úagent.message_sanitizationr   Úhermes_state_commonr   r	   r^   Ú	getLoggerÚloggerÚcompiler   r   re   r   r{   r   r   ú<module>r„      sy   ðñõ #ã Û 	ß &Ñ &å ;ß Sð 
ˆ×	Ñ	˜>Ó	*€ð �B—J‘JÐBÓCÐ Ø �b—j‘jÐ!fÓgÐ Ø�R—Z‘ZÐ 1Ó2Ð ÷o3ò o3r   