Ë
    âß²jçY ã                   óâ  — 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mZ ddl	m
Z
mZmZmZmZmZmZmZ ddlmZmZ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&m'Z'm(Z(m)Z*  ejV                  d«      Z,d	ee-e
f   d
ee-   fd„Z.dIde-d
e-fd„Z/ e0«       Z1de
d
ee-e
f   fd„Z2de-d
ee-ee-   f   fd„Z3de-d
ee-ee-   f   fd„Z4de› de› d�Z5dJdee-   de-d
e-fd„Z6ddddddddddœ	de7de-dee-   d e-d!ee-   de-d"e8d#e7d$e7d
eee-   ee
   f   fd%„Z9d&ee-   d
ee-   fd'„Z:d&ee-   d
ee-   fd(„Z;d)Z<d*Z=d+Z>d,Z?d-Z@ eAh d.£«      ZBd/ee-   d0e7d1ee-   d
e-fd2„ZCd3ZDd4j‹                  d5„ d6D «       «      ZFdJde-d7e-d
e-fd8„ZGd9ZHd:eHj‹                  g d;„ d<D «       ¢ eGd=d>d?eD› d@�z   «      ‘­«      z   dAz   ZIdj‹                  dB„ dCD «       «      ZJd:eHj‹                  dD„ dED «       «      z   dFz   eJz   ZK G dG„ dH«      ZLy)KzÌSession lifecycle mixin for SessionDB: row upsert/inheritance, lifecycle
flags (end/reopen/archive/pin/hide/read), model_config patching, listing and
counting, delete cascades, and the auto-archive sweep.é    N)ÚPath)ÚAnyÚCallableÚDictÚListÚOptionalÚSequenceÚTupleÚUnion)ÚActivityProvenanceÚbound_activity_descriptionÚnormalize_activity_provenance)Úreport_startup_progress)Ú_LISTABLE_CHILD_SQLÚ_PREVIEW_ELIGIBLE_SQLÚ_PREVIEW_RAW_SELECTÚ_RECOVERABLE_END_REASONSÚ_RECOVERABLE_END_REASONS_SQLÚ_RESET_CHILD_SQLÚ_RESET_END_REASONSÚ_legacy_reset_child_sqlÚ_shape_previewÚ_sql_json_extractÚ_sql_session_last_activeÚ_sql_session_last_active_by_idÚescape_likeÚ_SQL_IN_CHUNKÚ
_id_chunksÚ_placeholdersÚhermes_stateÚrowÚreturnc                 óš   — | j                  d«      xs dj                  «       xs' | j                  d«      xs dj                  «       xs dS )zwWorkspace grouping key: git repo root, else cwd, else None (branch excluded: a checkout must not
    fragment history).Úgit_repo_rootÚ ÚcwdN)ÚgetÚstrip)r!   s    ú:/home/ubuntu/.hermes/hermes-agent/hermes_state_sessions.pyÚworkspace_keyr*      sB   € ð �G‰G�OÓ$Ò*¨×1Ñ1Ó3Ò]¸¿¹À»Ò8LÈ"×7SÑ7SÓ7UÒ]ÐY]Ð]ó    Úcolc                 ó   — t        | d«      S )Nú$._delegate_from©r   )r,   s    r)   Ú_delegate_from_jsonr0   #   s   € Ü˜SÐ"4Ó5Ð5r+   Úrawc                 óê   — t        | t        «      r&| j                  «       r	 t        j                  | «      } t        | t        «      rt        | «      S i S # t        j
                  t        f$ r i cY S w xY w)zVTolerant ``model_config`` decode: JSON text or dict -> dict copy; anything else -> {}.)Ú
isinstanceÚstrr(   ÚjsonÚloadsÚJSONDecodeErrorÚ	TypeErrorÚdict)r1   s    r)   Ú_parse_model_configr:   ,   sa   € ä�#”sÔ §	¡	¤ð	Ü—*‘*˜S“/ˆCô # 3¬Ô-Œ4�‹9Ð5°2Ð5øô ×$Ñ$¤iÐ0ò 	ØŠIð	ús   ¢A ÁA2Á1A2Ú
cwd_prefixc                 ó\   — | j                  d«      xs | }t        |«      }d||› d�|› d�gfS )Nú/\zA(s.cwd = ? OR s.cwd LIKE ? ESCAPE '\' OR s.cwd LIKE ? ESCAPE '\')z/%z\\%)ÚrstripÚ_escape_like)r;   ÚprefixÚescs      r)   Ú_cwd_prefix_clauserB   6   sF   € Ø×Ñ˜uÓ%Ò3¨€Fô �vÓ
€CàMØ	�C�5˜� ˜u E˜]Ð+ðð r+   Úkeyc                 ó^   — | j                  d«      xs | }t        |«      \  }}d|› d�|g|¢fS )z�WHERE for ``workspace_key(row) == key``: git_repo_root equals ``key``, or (rows predating
    per-session git metadata) cwd is at/under ``key``.r=   z@(s.git_repo_root = ? OR (COALESCE(s.git_repo_root, '') = '' AND ú)))r>   rB   )rC   r@   Ú
cwd_clauseÚ
cwd_paramss       r)   Ú_workspace_key_clauserH   A   sH   € ð �Z‰Z˜ÓÒ% #€FÜ/°Ó7Ñ€J�
à
JÈ:È,ÐVXÐYØ	Ð�*Ððð r+   z*COALESCE(
                        (SELECT zª
                         FROM messages m
                         WHERE m.session_id = s.id AND m.role = 'user' AND m.content IS NOT NULL
                           AND z
                         ORDER BY m.timestamp, m.id LIMIT 1),
                        ''
                    ) AS _preview_rawr%   ÚclausesÚleadc                 ó6   — | r|› ddj                  | «      › �S dS )zG``WHERE a AND b`` (with *lead* prefix) or "" when there are no clauses.úWHERE ú AND r%   )Újoin)rI   rJ   s     r)   Ú
_where_sqlrO   X   s$   € á5<ˆdˆV�6˜'Ÿ,™, wÓ/Ð0Ð1ÐDÀ"ÐDr+   F©	Úexclude_childrenÚsourceÚsourcesÚsession_keyÚexclude_sourcesr;   Úmin_message_countÚarchived_onlyÚinclude_archivedrQ   rR   rS   rT   rU   rV   rW   rX   c        	         ó´  — g }	g }
| r|	t         t        d«      › d�gz  }	|r|gnt        |xs g «      }dt        |«      › d�|fd|r|gng fdt        |xs d«      › d�|xs g f|rt	        |«      ndg fd	|d
kD  r|gng ffD ]*  \  }}|sŒ	|	j                  |«       |
j                  |«       Œ, |r|	j                  d«       |	|
fS |s|	j                  d«       |	|
fS )a  Shared ``sessions s`` WHERE builder so counts line up with listed rows. ``exclude_children``
    hides sub-agent runs and compression continuations but keeps branch/reset children
    (``_LISTABLE_CHILD_SQL``). Clause order is part of the SQL text contract.ús.model_configú IS NULLús.source IN (ú)zs.session_key = ?ús.source NOT IN (© r%   zs.message_count >= ?r   zs.archived = 1ús.archived = 0)r   r0   ÚlistÚ_session_ids_placeholdersrB   ÚappendÚextend)rQ   rR   rS   rT   rU   r;   rV   rW   rX   ÚwhereÚparamsÚinclude_sourcesÚclauseÚvaluess                 r)   Ú_session_filter_whererj   ]   s%  € ð €EØ€FÙØÔ%Ô*=Ð>NÓ*OÐ)PÐPXÐ'YÐZÑZˆñ #)�v‘h¬d°7²=¸bÓ.A€OàÔ2°?ÓCÐDÀAÐ	FÈÐXØ	©{˜{™mÀÐCØÔ6°Ò7LÈ"ÓMÐNÈaÐ	PÐRaÒRgÐegÐhÙ+5Ô	˜JÔ	'¸BÀ¸8Ø	Ð8IÈAÒ8MÐ"3Ñ!4ÐSUÐVðò 	"‰ˆ�ò Ø�L‰L˜Ô Ø�M‰M˜&Õ!ð	"ñ Ø�‰Ð%Ô&ð �&ˆ=Ðñ Ø�‰Ð%Ô&Ø�&ˆ=Ðr+   Ú
parent_idsc                 óÊ  — t        «       }|D �ch c]  }|sŒ|’Œ	 }}t        |«      }t        |«      }|r‘g }t        |t        dz  «      D ]t  }t        |«      }	| j                  d|› d|	› d|	› d|› d�	||z   «      }
|
j                  «       D ]2  }|d   |vsŒ|j                  |d   «       |j                  |d   «       Œ4 Œv |}|rŒ‘|D �cg c]	  }||vsŒ|‘Œ c}S c c}w c c}w )zœDelegate-subagent ids (``_delegate_from`` marker, walked recursively) to cascade-delete with
    *parent_ids*; untagged children stay orphaned, not deleted.é   úSELECT id FROM sessions WHERE z IN (z) OR (parent_session_id IN (z) AND z IS NOT NULL)Úid)
r0   Úsetra   r   r   rb   ÚexecuteÚfetchallÚaddrc   )Úconnrk   ÚdfÚsidÚseedsÚfoundÚfrontierÚnext_frontierÚchunkÚphÚcursorr!   s               r)   Ú_collect_delegate_child_idsr~   ƒ   s  € ô 
Ó	€BØ&Ö.�Sª#ŠSÐ.€EÐ.ô ˜%“j€EÜ�E‹{€HÙ
Ø#%ˆÜ ¬-¸1Ñ*<Ó=ò 		4ˆEÜ*¨5Ó1ˆBØ—\‘\Ø0°°°E¸"¸ð >-Ø-/¨D°°r°d¸-ðIØJOÐRWÉ-óˆFð —‘Ó(ò 4�Ø�t‘9 EÒ)Ø—I‘I˜c $™iÔ(Ø!×(Ñ(¨¨T©Õ3ñ4ð		4ð !ˆò ð !Ö5�C C¨uÒ$4ŠCÒ5Ð5ùò/ /ùò. 6s   �C—CÃ
	C ÃC c                 óØ   — t        | |«      }t        |«      D ]O  }t        |«      }| j                  d|› d�|«       | j                  d|› d�|«       | j                  d|› d�|«       ŒQ |S )Nú*DELETE FROM messages WHERE session_id IN (r]   úIUPDATE sessions SET parent_session_id = NULL WHERE parent_session_id IN (ú"DELETE FROM sessions WHERE id IN ()r~   r   rb   rq   )rt   rk   Úidsr{   r|   s        r)   Ú_delete_delegate_childrenr„   ¡   s€   € Ü
% d¨JÓ
7€CÜ˜C“ò HˆÜ& uÓ-ˆØ�‰ÐAÀ"ÀÀQÐGÈÔOà�‰Ð`ÐacÐ`dÐdeÐfÐhmÔnØ�‰Ð9¸"¸¸QÐ?ÀÕGðHð €Jr+   )ÚkanbanÚtoolÚoneshotÚcompleteÚinterruptedÚerrorÚempty>   rŠ   Úagent_errorÚcontent_filterÚroleÚhas_tool_callsÚfinish_reasonc                 óÐ   — |xs dj                  «       j                  «       t        v rt        S | xs dj                  «       j                  «       }|dv s|dk(  r|rt        S t
        S )uÅ   Error finish â†’ ``error``; assistant with pending tool_calls or a trailing user/tool row â†’
    ``interrupted``; otherwise ``complete`` (benign default: pickers must not alarm on unknown shapes).r%   >   r†   ÚuserÚ	assistant)r(   ÚlowerÚ_ERROR_FINISH_REASONSÚSESSION_STATUS_ERRORÚSESSION_STATUS_INTERRUPTEDÚSESSION_STATUS_COMPLETE)rŽ   r�   r�   Úrs       r)   Úclassify_session_statusrš   ¼   sc   € ð 	Ò˜×"Ñ"Ó$×*Ñ*Ó,Ô0EÑEÜ#Ð#Ø	Š�×ÑÓ×"Ñ"Ó$€AØÐÑ  kÒ!1±nÜ)Ð)Ü"Ð"r+   zÏp.session_key IS NULL OR sessions.session_key IS NULL OR substr(p.session_key, 1, instr(substr(p.session_key, 7), ':') + 6)  = substr(sessions.session_key, 1, instr(substr(sessions.session_key, 7), ':') + 6)ú,
c              #   ó4   K  — | ]  }d |› d|› d|› d�–— Œ y­w)z                       ú = COALESCE(sessions.z, excluded.r]   Nr_   )Ú.0r,   s     r)   ú	<genexpr>rŸ   Ö   s,   è ø€ ò 'ØSVÐ˜c˜UÐ"7¸°u¸KÈÀuÈAÔNñ'ùs   ‚)rT   Úchat_idÚ	chat_typeÚ	thread_idÚparent_session_idr&   Úprofile_nameÚtransport_profiler$   Úorigin_jsonÚdisplay_nameÚextrac                 óL   — ddt        | «      z   z  }| › d| › d|› d| › d|› d|› d�S )	zb``col = COALESCE(sessions.col, (SELECT p.col FROM parent))`` (whitespace is part of the SQL text).Ú é   r�   r›   z
(SELECT p.z FROM sessions p
z)  WHERE p.id = sessions.parent_session_idrE   )Úlen)r,   r¨   Úpads      r)   Ú_inherit_col_sqlr®   Þ   sH   € à
�”c˜#“h‘Ñ
€Càˆ%Ð$ S E¨¨S¨E°¸C¸5Ð@RØˆ%Ð8¸¸¸rð	Cðr+   z,
                           z+UPDATE sessions
                       SET c              #   ó2   K  — | ]  }t        |«      –— Œ y ­w©N©r®   ©rž   Úcs     r)   rŸ   rŸ   ë   s   è ø€ Ò	N !Ô
˜1×
Ñ	Nùó   ‚)r&   r$   Ú
git_branchr¤   z/
                                              zAND (r]   zD
                     WHERE id = ? AND parent_session_id IS NOT NULLc              #   óB   K  — | ]  }d t        dd|› �«      › d�–— Œ y­w)z%
                       AND COALESCE(Úmodel_configz$.z5, '')
                           != parent_session_idNr/   )rž   Úmarkers     r)   rŸ   rŸ   ô   s;   è ø€ ò #ð 	ð -Ô->¸~ÐQSÐTZÐS[È}Ó-]Ð,^ð _8ô 8ñ#ùs   ‚)Ú_delegate_fromÚ_branched_fromc              #   ó2   K  — | ]  }t        |«      –— Œ y ­wr°   r±   r²   s     r)   rŸ   rŸ   û   s   è ø€ ò °Ô(¨×+ñ ùr´   )Úuser_idrT   r    r¡   r¢   r§   r¦   r¥   a6  
                     WHERE id = ? AND parent_session_id IS NOT NULL
                       AND EXISTS (
                           SELECT 1 FROM sessions p
                           WHERE p.id = sessions.parent_session_id
                             AND p.end_reason = 'compression'
                       )c            *       ó8	  — e Zd ZU dZdee   fd„Zededdfd„«       Z	 	 	 	 	 	 d¬dededed	e	ee
f   d
ededee   dedededededee   dedededee   ddf$d„Zdededefd„Zd­dedededefd„Zd®dededdfd„Zdddœdededee   dee   dee   f
d„ZdZd  ed!d"«      › d# ed!d$«      › d# ed!d%«      › d&�Zded'eddfd(„Zd)ed*eded+edef
d,„Zdeddfd-„Zd¯ded+edefd.„Z	 	 d°deded0ee   dee   d1edee   fd2„Z	 	 d±deded3ed0ee   dee   defd4„Zd5e	eef   ddfd6„Z	 d²ddd7œded8ee   d9ee   d:ee   ddf
d;„Zdeddfd<„Z 	 d²ded=edee   ddfd>„Z!ded
ee   ddfd?„Z"ded@ee#e      ddfdA„Z$d²dededBee   ddfdC„Z%	 	 d³dedDe	ee
f   d)ed*ee&ee   gef      ddf
dE„Z'dFdGœdedDe	ee
f   dHefdI„Z(dedDe	ee
f   ddfdJ„Z)d²dedKedLe
de
fdM„Z*ddddd/dNœdedee   dBee   dOee	ee
f      dPee   dQeddfdR„Z+dedSeddfdT„Z,edUee	ee
f      defdV„«       Z-dedee	ee
f      fdW„Z.dedee	ee
f      fdX„Z/dYedee   fdZ„Z0dedefd[„Z1defd\„Z2d]eded^e
defd_„Z3ded`edefda„Z4e5Z6dedefdb„Z7dedcedefdd„Z8dedeedefdf„Z9d®dedgedefdh„Z:edie	ee
f   defdj„«       Z; e<h dk£«      Z=dZ>ee   e?dl<   edmednedoede@ee#e
   f   fdp„«       ZAdqe#e	ee
f      drede#e	ee
f      fds„ZBdtddudddvœdwedxe#e   dyedzed{ede#e	ee
f      fd|„ZCeDd}eEjŒ                  de	ee
f   fd~„«       ZG	 	 	 	 	 	 d´dede#e   dxe#e   d€edwed�ed‚edƒed„ed…ed†ed‡edˆed‰edredŠeded‹ede#e	ee
f      f&dŒ„ZHd�e#e   de	eef   fdŽ„ZId²ded�ee   defd�„ZJdedefd‘„ZKdede#e   fd’„ZL	 	 dµdeMeeNe   df   dwed�ed“ede#e	ee
f      f
d”„ZO	 	 	 d¶dede#e   d€edƒed†ed‡ed•edxe#e   defd–„ZPd·d—edefd˜„ZQd/d/d/d™œd†ed‡ed•ede	eef   fdš„ZRdede@eef   fd›„ZSedœeeT   deddfd�„«       ZUdede#e   fdž„ZV	 	 d±dedœeeT   dŸee#e      defd „ZWd²dedœeeT   defd¡„ZXd²d�e#e   dœeeT   defd¢„ZYd£ZZdefd¤„Z[d²dœeeT   defd¥„Z\	 d±d¦ee   dedefd§„Z]	 d¸d¨ed©edªede	ee
f   fd«„Z^y)¹ÚSessionSessionsMixinzOSession rows: create/inherit, lifecycle flags, model_config, listing, deletion.r"   c                 ón  — 	 ddl m}  |«       j                  «       }t        | j                  «      j                  «       j
                  }||k(  ry|j
                  |dz  k(  }|r,t        j                  d|j                  «      r|j                  S y	# t        $ r t        j                  dd¬«       Y y	w xY w)
u  The profile owning THIS store, from ``db_path`` alone (``<root>/state.db`` â†’ default,
        ``<root>/profiles/<name>/state.db`` â†’ name): a gateway serving a NON-launch profile opens that
        profile's store. None outside the profile tree â€” NULL beats a fabricated owner.r   )Úget_default_hermes_rootÚdefaultÚprofilesz[a-z0-9][a-z0-9_-]{0,63}zown-profile derivation failedT)Úexc_infoN)Úhermes_constantsrÀ   Úresolver   Údb_pathÚparentÚreÚ	fullmatchÚnameÚ	ExceptionÚloggerÚdebug)ÚselfrÀ   ÚrootrÇ   Úis_profile_dirs        r)   Ú_own_profile_namez&SessionSessionsMixin._own_profile_name  s¡   € ð
	IÝ@Ù*Ó,×4Ñ4Ó6ˆDÜ˜$Ÿ,™,Ó'×/Ñ/Ó1×8Ñ8ˆFØ˜Š~Ø Ø#Ÿ]™]¨d°ZÑ.?Ñ?ˆNÙ¤"§,¡,Ð/JÈFÏKÉKÔ"XØ—{‘{Ð"ð øô ò 	IÜ�L‰LÐ8À4ˆLÕHØð	Iús   ‚AB Á?B Â B4Â3B4Ú
session_idNc                 ó`   — | j                  t        |f«       | j                  t        |f«       y)ug  NULL-fill a child's cwd/git/profile from its parent (profile_name only within the same
        ``agent:<ns>:`` namespace). Gateway routing columns are inherited ONLY by compression forks
        (a crash before the gateway re-records the peer would strand the child unroutable); delegate
        and branch children must NOT inherit them (peer recovery could repoint traffic into a
        subagent's session), including when their parent row itself ended on compression â€” a long
        batch outlives its coordinator's rotation, and two live rows holding one routing key is the
        shape reported in #92859.N)rq   Ú_INHERIT_PARENT_META_SQLÚ_INHERIT_PARENT_ROUTING_SQL©rt   rÒ   s     r)   Ú _inherit_parent_session_metadataz5SessionSessionsMixin._inherit_parent_session_metadata  s&   € ð 	�‰Ô-°
¨}Ô=Ø�‰Ô0°:°-Õ@r+   rR   Úmodelr·   Úsystem_promptr¼   rT   r    r¡   r¢   r£   r&   r¤   r$   r¦   r§   r¥   c                 óÖ   ‡ ‡‡‡‡‡‡‡‡‡	‡
‡‡‡‡‡‡‡— ‰xs dj                  «       s‰ j                  «       Šˆˆ	ˆˆˆˆˆˆˆˆˆ ˆˆˆˆˆ
ˆˆfd„}‰ j                  |‰ j                  ¬«       y)ui
  Upsert a session row, never overwriting what an earlier writer set (the gateway creates a
        bare row before create_session carries the real model/prompt) â€” the one exception is the
        token-accounting guard's placeholder ``source='unknown'``, which a later writer's real surface
        replaces (#111999): once minted, that placeholder otherwise labelled a real session anonymous
        for life, because this upsert is the only writer that could correct it. chat_id/thread_id scope gateway
        /resume (IDOR). Children backfill from the parent; a missing profile_name is stamped with THIS
        store's own (NULL reads as unowned).

        When ``parent_session_id`` is set (compression fork, delegate/subagent spawn, branch continuation)
        and this row's own ``cwd``/``git_repo_root``/ ``git_branch``/``profile_name`` are still NULL after
        the insert, they are backfilled from the parent row. Callers of ``create_session`` for a child
        session historically didn't propagate these fields themselves (e.g. the compression-fork path), so a
        lineage could silently lose its working directory and drop out of the project sidebar every time it
        forked (#64709), or lose its owning profile and be aggregated as "default" every time it rotated or
        branched (the cross-profile session-jump bug). This only fills NULLs â€” an explicit value on the
        child is never overwritten. For compression forks specifically (parent ended with
        ``end_reason='compression'``), the gateway origin columns
        (``user_id``/``session_key``/``chat_id``/``chat_type``/
        ``thread_id``/``display_name``/``origin_json``) are inherited too, so a crash before the gateway
        re-records the peer can't strand the child without a recoverable routing mapping (#59527).
        When the caller passes no ``profile_name`` at all, the row is stamped with THIS store's own profile
        (:meth:`_own_profile_name`) instead of NULL. Every ``state.db`` belongs to exactly one profile â€” the
        same single-match contract :meth:`backfill_null_session_profiles` relies on â€” so the stamp is
        derivation, not a guess. Rows minted NULL after that one-shot #94724 backfill ran stayed NULL
        forever, and profile-keyed consumers (desktop sidebar scope matching, ``@session:<profile>/<id>``
        deep links, the fail-closed owner ladder) treat NULL as unowned: the session vanishes from the
        sidebar even though its transcript is intact (#99222). Stores outside the profile tree (explicit
        ``db_path`` in tests, ad-hoc copies) derive nothing and keep NULL â€” never guess.
        r%   c                 ó"  •— ‰j                  | ‰«      }| j                  dt        z   ‰‰‰‰‰‰‰‰‰rt        j                  ‰«      nd |‰
‰‰‰‰‰	‰t        j
                  «       f«       |�‰j                  | «       ‰
r‰j                  | ‰«       y y )NaÀ  INSERT INTO sessions (
                   id, source, user_id, session_key, chat_id, chat_type, thread_id,
                   model, model_config, system_prompt, system_prompt_hash,
                   parent_session_id, cwd, profile_name, transport_profile, git_repo_root,
                   origin_json, display_name, started_at
                )
                   VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                   ON CONFLICT(id) DO UPDATE SET
                       source = CASE
                           WHEN sessions.source = 'unknown'
                           THEN COALESCE(excluded.source, 'unknown')
                           ELSE sessions.source
                       END,
                       model = COALESCE(sessions.model, excluded.model),
                       model_config = CASE
                           WHEN excluded.model_config IS NOT NULL
                                AND json_type(
                                    sessions.model_config, '$._reset_from'
                                ) IS NOT NULL
                                AND json_remove(
                                    sessions.model_config, '$._reset_from'
                                ) = '{}'
                           THEN json_set(
                               excluded.model_config,
                               '$._reset_from',
                               json_extract(
                                   sessions.model_config, '$._reset_from'
                               )
                           )
                           ELSE COALESCE(
                               sessions.model_config, excluded.model_config
                           )
                       END,
                       system_prompt_hash = COALESCE(
                           sessions.system_prompt_hash,
                           excluded.system_prompt_hash
                       ),
                       system_prompt = CASE
                           WHEN sessions.system_prompt_hash IS NULL
                                AND excluded.system_prompt_hash IS NOT NULL
                           THEN NULL
                           ELSE sessions.system_prompt
                       END,
)Ú_store_system_promptrq   Ú_UPSERT_KEEP_EXISTING_SQLr5   ÚdumpsÚtimeÚ#_delete_unreferenced_system_promptsr×   )rt   Úsystem_prompt_hashr    r¡   r&   r§   r$   rØ   r·   r¦   r£   r¤   rÎ   rÒ   rT   rR   rÙ   r¢   r¥   r¼   s     €€€€€€€€€€€€€€€€€€r)   Ú_doz5SessionSessionsMixin._insert_session_row.<locals>._doP  s«   ø€ Ø!%×!:Ñ!:¸4ÀÓ!OÐØ�L‰Lð+ôV  ñW+ ðZ  ¨°¸gÀyÐR[Ð]bÙ0<”D—J‘J˜|Ô,À$ÐHZØ% s¨LÐ:KÈ]Ð\gØ ¤$§)¡)£+ð	ô[3ðh "Ð-Ø×8Ñ8¸Ô>Ù Ø×5Ñ5°d¸JÕGð !r+   ©Ú
patience_sN)r(   rÑ   Ú_execute_writeÚ_TRANSCRIPT_WRITE_PATIENCE_S)rÎ   rÒ   rR   rØ   r·   rÙ   r¼   rT   r    r¡   r¢   r£   r&   r¤   r$   r¦   r§   r¥   râ   s   `````````````````` r)   Ú_insert_session_rowz(SessionSessionsMixin._insert_session_row)  sU   ÿÿù€ ðJ Ò" ×)Ñ)Ô+Ø×1Ñ1Ó3ˆL÷9	H÷ 9	Hõ 9	Hðv 	×Ñ˜C¨D×,MÑ,MÐÕNr+   c                 ó.   —  | j                   ||fi |¤Ž |S )z9Create (upsert) a session record. Returns the session_id.©rç   )rÎ   rÒ   rR   Úkwargss       r)   Úcreate_sessionz#SessionSessionsMixin.create_session�  s   € à ˆ× Ñ  ¨VÑ>°vÒ>ØÐr+   c                 ó2   —  | j                   ||fd|i|¤Ž |S )z>Ensure a session row exists (upsert). Accepts optional kwargs.rØ   ré   )rÎ   rÒ   rR   rØ   rê   s        r)   Úensure_sessionz#SessionSessionsMixin.ensure_session’  s$   € à ˆ× Ñ  ¨VÑK¸5ÐKÀFÒKØÐr+   Ú	finalizedc                 ó:   — |sy| j                  d|rdnd|f«       y)zjMirror ``SessionEntry.expiry_finalized`` so it survives a lost sessions.json.

        See #9006.
        Nz5UPDATE sessions SET expiry_finalized = ? WHERE id = ?é   r   )Ú
_write_sql)rÎ   rÒ   rî   s      r)   Úset_expiry_finalizedz)SessionSessionsMixin.set_expiry_finalized—  s&   € ñ
 ØØ�‰ØCÉ9ÁaÐZ[Ð]gÐEhõ	
r+   )r¢   r¼   Úplatformc                ó  — |r|dv ryd}|t        |«      g}|�|dz  }|j                  t        |«      «       | j                  |dz   |«      D �cg c]  }t        |«      ‘Œ }}|sy|r\|D �cg c]/  }t        |j	                  d«      xs d«      t        |«      k(  sŒ.|‘Œ1 }	}|	rt        |	d   d	   «      S t        |«      d
kD  r(yt        d„ |D «       D �
ch c]  }
|
sŒ|
’Œ	 c}
«      d
kD  ryt        |d   d	   «      S c c}w c c}w c c}
w )uÆ   Most recent live session_id for source + chat_id (+ thread_id). With ``user_id`` exact sender
        matches win; several distinct users and no match â†’ None (never another participant's session).)Nr%   NzØ
            SELECT id, user_id, started_at FROM sessions
            WHERE LOWER(source) = LOWER(?)
              AND session_key IS NOT NULL
              AND chat_id = ?
              AND ended_at IS NULL
        z  AND COALESCE(thread_id, '') = ?z ORDER BY started_at DESCr¼   r%   r   ro   rð   c              3   ót   K  — | ]0  }t        |j                  d «      xs d«      j                  «       –— Œ2 y­w)r¼   r%   N)r4   r'   r(   )rž   r™   s     r)   rŸ   z>SessionSessionsMixin.find_session_by_origin.<locals>.<genexpr>¾  s,   è ø€ ÒPÀaœc !§%¡%¨	Ó"2Ò"8°bÓ9×?Ñ?×AÑPùs   ‚68)r4   rc   Ú	_read_allr9   r'   r¬   )rÎ   ró   r    r¢   r¼   Úqueryrf   r™   ÚrowsÚexactÚus              r)   Úfind_session_by_originz+SessionSessionsMixin.find_session_by_origin¢  s  € ñ ˜7 jÑ0Øðˆð !¤# g£,Ð/ˆØÐ ØÐ7Ñ7ˆEØ�M‰Mœ#˜i›.Ô)Ø!%§¡°Ð8SÑ0SÐU[Ó!\Ö]˜A”�Q•Ð]ˆÐ]ÙØÙØ $ÖT˜1¬¨A¯E©E°)Ó,<Ò,BÀÓ(CÄsÈ7Ã|Ó(S’QÐTˆEÐTÙÜ˜5 ™8 D™>Ó*Ð*Ü�4‹y˜1Š}ØÜÑPÈ4ÔPÖV˜ÒTU’!ÒVÓWÐZ[Ò[ØÜ�4˜‘7˜4‘=Ó!Ð!ùò ^ùò Uùò
 Ws   ÁC<Á,/DÂDÃDÃDç      Œ@z  AND COALESCE(z{alias}model_configú$._branched_fromz, '') != ?
  AND COALESCE(r.   ú$._reset_fromz7, '') != ?
  AND COALESCE({alias}source, '') != 'tool'
Ú
end_reasonc                 ó6   ‡ ‡‡— ‰ j                  ˆˆ ˆfd„«       y)z°Mark a session ended; the first end_reason wins (a compression split must keep
        ``'compression'`` even if a stale end_session() lands later); reopen_session() to re-end.c                 óX   •— ‰j                  | dt        j                  «       ‰‰f‰‰«      S )NzRUPDATE sessions SET ended_at = ?, end_reason = ? WHERE id = ? AND ended_at IS NULL)Ú_end_and_bumprß   )rt   rÿ   rÎ   rÒ   s    €€€r)   ú<lambda>z2SessionSessionsMixin.end_session.<locals>.<lambda>Ô  s,   ø€ ¨×);Ñ);ØÐfÜ�Y‰Y‹[˜* jÐ1°:¸zó*
€ r+   N©rå   )rÎ   rÒ   rÿ   s   ```r)   Úend_sessionz SessionSessionsMixin.end_sessionÑ  s   ú€ ð 	×Ñõ 
õ 	r+   Úsqlrf   Úreasonc                 óh   — |j                  ||«      j                  }|r| j                  |||«       |S )z¤Run an end-stamp UPDATE; only a boundary this call actually wrote advances the
        conversation generation (a no-op must not rotate the peer). Returns rowcount.)rq   ÚrowcountÚ_bump_conversation_generation)rÎ   rt   r  rf   rÒ   r  Úchangeds          r)   r  z"SessionSessionsMixin._end_and_bumpÙ  s4   € ð —,‘,˜s FÓ+×4Ñ4ˆÙØ×.Ñ.¨t°ZÀÔHØˆr+   c                 ó2   ‡— ˆfd„}| j                  |«       y)aˆ  Clear ended_at/end_reason so a session can be resumed; first freeze markerless legacy reset
        children, skipping explicit fork/delegate provenance and children that predate the parent itself.
        The guard compares against the parent's started_at, not its current ended_at: a parent that was
        reopened and re-ended later still owns reset children from its earlier boundaries.c                 óâ   •— | j                  dt        dd«      › dt        dd«      › dt        dd«      › dt        dt        t        «      «      › �‰gt        ¢­«       | j                  d	‰f«       y )
Nz©UPDATE sessions AS child SET model_config = json_set(COALESCE(child.model_config, '{}'), '$._reset_from', child.parent_session_id) WHERE child.parent_session_id = ? AND úchild.model_configrþ   z IS NULL AND rý   r.   z™ IS NULL AND COALESCE(child.source, '') != 'tool' AND child.started_at >= (SELECT p.started_at FROM sessions p WHERE p.id = child.parent_session_id) AND ÚchildúCUPDATE sessions SET ended_at = NULL, end_reason = NULL WHERE id = ?)rq   r   r   rb   r   rÖ   s    €r)   râ   z0SessionSessionsMixin.reopen_session.<locals>._doæ  s”   ø€ Ø�L‰Lð9ä9JÐK_ÐapÓ9qÐ8rð sÜ(Ð)=Ð?QÓRÐSð TÜ(Ð)=Ð?QÓRÐSð Tô /¨wÔ8QÔRdÓ8eÓfÐgðið Ð1Ô0Ñ1ô
ð �L‰LØUÐXbÐWdõr+   Nr  )rÎ   rÒ   râ   s    ` r)   Úreopen_sessionz#SessionSessionsMixin.reopen_sessioná  s   ø€ ô
	ð 	×Ñ˜CÕ r+   c                 ó˜   ‡ ‡‡‡— ‰syt        j                   «       Š	 t        ‰ j                  ˆˆˆ ˆfd„«      «      S # t        $ r Y yw xY w)ar  Durably mark an intentional reset boundary on live rows or rows with a *recoverable* accidental
        end_reason (explicit boundaries are preserved): an ``agent_close`` row left recoverable would be
        resurrected by stale-route recovery. Keep in sync with find_latest_gateway_session_for_peer.

        Plain ``end_session()`` is NOT sufficient for reset boundaries: it no-ops on an already-ended row,
        so a row that agent cleanup already closed as ``agent_close`` would stay recoverable and stale-route
        recovery would resurrect the reset session with its full history (#61220, #61993, #63539).
        Fc                 óD   •— ‰j                  | dt        › d�‰‰‰f‰‰«      S )NzfUPDATE sessions SET ended_at = ?, end_reason = ? WHERE id = ? AND (ended_at IS NULL OR end_reason IN (rE   )r  r   )rt   Únowr  rÎ   rÒ   s    €€€€r)   r  z?SessionSessionsMixin.promote_to_session_reset.<locals>.<lambda>  s6   ø€ ¸×9KÑ9KØð %Ü%AÐ$BÀ"ðFà�f˜jÐ)¨:°vó:€ r+   )rß   Úboolrå   rË   )rÎ   rÒ   r  r  s   ```@r)   Úpromote_to_session_resetz-SessionSessionsMixin.promote_to_session_reset÷  sN   û€ ñ ØÜ�i‰i‹kˆð	Ü˜×+Ñ+ö -ó ó ð øô
 ò 	Ùð	ús   �= ½	A	ÁA	Frµ   Úreplace_git_metac                 óš   ‡‡‡‡‡— ‰r‰sy|xs dj                  «       Š|xs dj                  «       Šˆˆˆˆˆfd„}| j                  |«      S )ax  Persist the authoritative cwd and claim a Git metadata generation. git fields are written
        only when non-empty (a probe failure never clobbers a value) except under ``replace_git_meta``
        (a workspace MOVE overwrites the old repo identity). Async probes publish with the returned
        generation so an older worker cannot overwrite a newer claim (A -> B -> A).Nr%   c                 óð  •— | j                  d‰f«      j                  «       }|€y ddg}‰g}|d   ‰k7  s‰	r-|j                  d«       |j                  ‰xs d ‰
xs d f«       n8d‰fd‰
ffD ]-  \  }}|sŒ	|j                  |› d�«       |j                  |«       Œ/ | j                  d	d
j	                  |«      › d�g |¢‰‘«       | j                  d‰f«      j                  «       }|€d S t        |d   «      S )Nz%SELECT cwd FROM sessions WHERE id = ?zcwd = ?zBgit_metadata_generation = COALESCE(git_metadata_generation, 0) + 1r   )zgit_branch = ?zgit_repo_root = ?rµ   r$   ú = ?úUPDATE sessions SET ú, z WHERE id = ?z9SELECT git_metadata_generation FROM sessions WHERE id = ?)rq   Úfetchonerd   rc   rN   Úint)rt   ÚcurrentÚsetsrf   r,   Úvalr!   Úbranchr&   r  Ú	repo_rootrÒ   s          €€€€€r)   râ   z4SessionSessionsMixin.update_session_cwd.<locals>._do  s  ø€ Ø—l‘lÐ#JÈZÈMÓZ×cÑcÓeˆGØˆØØÐcÐdˆDØ!$ ˆFØ�q‰z˜SÒ Ñ$4Ø—‘ÐCÔDØ—‘˜vš~¨¨yÒ/@¸DÐAÕBà".°Ð!7¸/È9Ð9UÐ Vò +‘H�C˜ÚØŸ™ s e¨4 LÔ1ØŸ™ cÕ*ð+ð �L‰LÐ/°·	±	¸$³Ð/@ÀÐNÐPeÐRXÐPeÐZdÐPeÔfØ—,‘,ØKÈjÈ]óç‰h‹jð ð ˜;�4Ð7¬C°°A±«KÐ7r+   )r(   rå   )	rÎ   rÒ   r&   rµ   r$   r  râ   r"  r#  s	    ``  ` @@r)   Úupdate_session_cwdz'SessionSessionsMixin.update_session_cwd  sP   ü€ ñ ¡ØØÒ" ×)Ñ)Ó+ˆØ"Ò( b×/Ñ/Ó1ˆ	÷	8ð 	8ð& ×"Ñ" 3Ó'Ð'r+   Ú
generationc           	      ó–  — t        |t        «      xr t        |t        «       xr |dk\  }|r|r|syd|xs dj                  «       fd|xs dj                  «       ffD ��cg c]  \  }}|r||f‘Œ }	}}|	sy| j	                  ddj                  d„ |	D «       «      › d	�|	D �
�cg c]  \  }
}|‘Œ	 c}}
|||gz   «      dk(  S c c}}w c c}}
w )
zAPublish async Git enrichment only while its cwd claim is current.rð   Frµ   r%   r$   r  r  c              3   ó,   K  — | ]  \  }}|› d �–— Œ y­w)r  Nr_   )rž   r,   Ú_s      r)   rŸ   zDSessionSessionsMixin.publish_session_git_metadata.<locals>.<genexpr>?  s   è ø€ Ò,O¹f¸cÀ1°¨u°D¬\Ñ,Oùs   ‚z9 WHERE id = ? AND cwd = ? AND git_metadata_generation = ?)r3   r  r  r(   Ú_write_rowcountrN   )rÎ   rÒ   r&   r%  rµ   r$   Úvalid_generationr,   r!  Úfieldsr(  s              r)   Úpublish_session_git_metadataz1SessionSessionsMixin.publish_session_git_metadata/  s  € ô
 & j´#Ó6Òo¼zÈ*ÔVZÓ?[Ð;[ÒoÐ`jÐnoÑ`oÐÙ¡Ñ,<Øð  
Ò 0¨b×7Ñ7Ó9Ð:¸_È}ÒObÐ`b×NiÑNiÓNkÐ<lð(÷
Ù#˜3 áð �#ŠJð
ˆñ 
ñ
 ØØ×#Ñ#Ø" 4§9¡9Ñ,OÈÔ,OÓ#OÐ"Pð QGð Gà%×&‘V�Q˜ŠSÓ&¨*°c¸:Ð)FÑFó
ð ñ	ð 	ùó
ùó 's   Á!B?Â"CÚcwd_to_rootc                 ó’   — |j                  «       D ��cg c]  \  }}|sŒ	|sŒ||f‘Œ }}}|r| j                  d|d¬«       yyc c}}w )zMBackfill git repo roots for cwds without one; never clobbers a recorded root.zXUPDATE sessions SET git_repo_root = ? WHERE cwd = ? AND COALESCE(git_repo_root, '') = ''T)ÚmanyN)Úitemsrñ   )rÎ   r-  r&   rÏ   Úpairss        r)   Úbackfill_repo_rootsz(SessionSessionsMixin.backfill_repo_rootsD  sQ   € à.9×.?Ñ.?Ó.A×R¡  dÂTÊc�$˜’ÐRˆÑRÙØ�O‰OØjØ˜Dð õ ð ùó Ss   ”
AŸA¢A)ÚdescriptionÚ
provenanceÚtsr3  r4  c                óÊ   — |syt        |�|nt        j                  «       «      }| j                  d|t        |«      t	        |«      j
                  ||f| j                  ¬«       y)a¨  Stamp durable mid-turn activity (observation-only; rate-limited by the caller) so surfaces see
        activity before any message row lands. Never moves ``last_activity_at`` backwards.

        Called (rate-limited) from ``AIAgent._touch_activity`` so gateway/CLI surfaces and stall consumers
        observe API/tool/compaction activity even when no new message row has been written yet (#72016 /
        #72039).
        Nz©UPDATE sessions SET last_activity_at = ?, last_activity_description = ?, last_activity_provenance = ? WHERE id = ? AND (last_activity_at IS NULL OR last_activity_at < ?)rã   )Úfloatrß   rñ   r   r   ÚvalueÚ_ACTIVITY_WRITE_PATIENCE_S)rÎ   rÒ   r5  r3  r4  Úwhens         r)   Útouch_session_activityz+SessionSessionsMixin.touch_session_activityM  sf   € ñ ØÜ˜2˜>‘R¬t¯y©y«{Ó;ˆØ�‰ðRð Ô0°Ó=Ü-¨jÓ9×?Ñ?ÀÈTðð ×6Ñ6ð 	õ 		
r+   c                 ó4  — |sy	 | j                  d|f«      }|�+|d   s&|d   r |d   t        j                  j
                  k(  ry| j                  ddt        j                  j
                  |f| j                  ¬«       y# t        j                  $ r d}Y Œ~w xY w)aÖ  Clear activity labels after a turn (``last_activity_at`` is kept so idle / watchdog clocks stay
        continuous). A no-op clear skips the write transaction.

        Description and provenance are observation labels for *what was happening at* that timestamp during
        an active turn; once the turn is idle they must not keep advertising "compressing" / "executing
        tool" (#72039).
        Response-critical-path contract (#76354 review S1): runs in the turn's ``finally``; a no-op clear
        (labels already empty) skips the write transaction entirely, and a real clear uses the same short
        sub-second busy budget as :meth:`touch_session_activity` instead of the full routine write patience.
        NzUSELECT last_activity_description, last_activity_provenance FROM sessions WHERE id = ?r   rð   z\UPDATE sessions SET last_activity_description = ?, last_activity_provenance = ? WHERE id = ?r%   rã   )Ú	_read_oneÚsqlite3ÚErrorr   ÚUNKNOWNr8  rñ   r9  ©rÎ   rÒ   r!   s      r)   Úclear_session_activity_labelsz2SessionSessionsMixin.clear_session_activity_labelsi  s    € ñ Øð	Ø—.‘.ØgØ�óˆCð ˆ? 3 q¢6°3°q²6¸SÀ¹VÔGY×GaÑGa×GgÑGgÒ=gØØ�‰ØjØÔ#×+Ñ+×1Ñ1°:Ð>È4×KjÑKjð 	õ 	
øô	 �}‰}ò 	ØŠCð	ús   …A? Á?BÂBÚmodel_config_jsonc                 óN   — | j                  «        | j                  d|||f«       y)z4Update model_config and (COALESCE) optionally model.zMUPDATE sessions SET model_config = ?, model = COALESCE(?, model) WHERE id = ?N)Úflush_token_countsrñ   )rÎ   rÒ   rC  rØ   s       r)   Úupdate_session_metaz(SessionSessionsMixin.update_session_meta„  s)   € ð 	×ÑÔ!Ø�‰Ø[Ø  zÐ2õ	
r+   c                 ó:   ‡ ‡‡— ˆ ˆˆfd„}‰ j                  |«       y)z0Store the full assembled system prompt snapshot.c                 óp   •— | j                  d‰j                  | ‰«      ‰f«       ‰j                  | «       y )NzMUPDATE sessions SET system_prompt_hash = ?, system_prompt = NULL WHERE id = ?)rq   rÜ   rà   )rt   rÎ   rÒ   rÙ   s    €€€r)   râ   z6SessionSessionsMixin.update_system_prompt.<locals>._do�  s7   ø€ Ø�L‰LØ_Ø×*Ñ*¨4°Ó?ÀÐLôð ×4Ñ4°TÕ:r+   Nr  )rÎ   rÒ   rÙ   râ   s   ``` r)   Úupdate_system_promptz)SessionSessionsMixin.update_system_promptŽ  s   ú€ ö	;ð 	×Ñ˜CÕ r+   Ú
tool_namesc                 óp   — |�t        j                  t        |«      «      nd}| j                  d||f«       y)zšPersist the resolved ``tools[]`` name order so a rebuilt AIAgent can't fork the cached tool
        prefix on a flipped check_fn verdict; ``None`` clears.Nz/UPDATE sessions SET tool_names = ? WHERE id = ?)r5   rÞ   ra   rñ   )rÎ   rÒ   rJ  Úpayloads       r)   Úupdate_session_tool_namesz.SessionSessionsMixin.update_session_tool_names˜  s3   € ð 3=Ð2H”$—*‘*œT *Ó-Ô.ÈdˆØ�‰ÐIÈGÐU_ÐK`Õar+   Úproviderc                 ó|   ‡‡— | j                  «        ddi}‰r‰|d<   |r||d<   | j                  ‰|dˆˆfd„«       y)a�  Set the model after a mid-session /model switch (unconditionally), null system_prompt so
        stale Model:/Provider: footers rebuild, and drop any Browser runtime lock (lineage markers
        survive). *provider* is merged into model_config so resume recombines model and provider.

        When *provider* is given, it is merged into ``model_config`` alongside the model (``$.model`` /
        ``$.provider``) so a later resume recombines the persisted model with the provider that actually
        serves it instead of the config.yaml primary provider (#79536). Callers without provider knowledge
        leave any stored provider untouched.
        Úbrowser_model_lockNrØ   rN  zmUPDATE sessions SET model = ?, model_config = ?, system_prompt = NULL, system_prompt_hash = NULL WHERE id = ?c                 ó   •— ‰| ‰fS r°   r_   ©ÚmergedrØ   rÒ   s    €€r)   r  z;SessionSessionsMixin.update_session_model.<locals>.<lambda>³  s   ø€ ˜E 6¨:Ð6€ r+   )rE  Ú_write_model_config_patch)rÎ   rÒ   rØ   rN  Úpatchs    ``  r)   Úupdate_session_modelz)SessionSessionsMixin.update_session_modelž  sP   ù€ ð 	×ÑÔ!Ø!5°tÐ <ˆÙØ"ˆE�'‰NÙØ (ˆE�*ÑØ×&Ñ&Ø˜ð  Kä6õ	
r+   rU  c                 óB   ‡ ‡‡‡‡— ˆˆˆ ˆˆfd„}‰ j                  |«       y)zÀMerge ``patch`` into model_config then run ``sql`` with ``params(merged)`` in one write
        transaction; no-op when the row doesn't exist. Custom ``sql`` (prompt-nulling) also GCs prompts.c                 ó¢   •— ‰j                  | ‰‰«      }|t        u ry | j                  ‰‰r ‰|«      n|‰f«       ‰�‰j                  | «       y y r°   )Ú_merge_model_config_jsonÚ_MODEL_CONFIG_ROW_MISSINGrq   rà   )rt   rS  rf   rU  rÎ   rÒ   r  s     €€€€€r)   râ   z;SessionSessionsMixin._write_model_config_patch.<locals>._do½  sW   ø€ Ø×2Ñ2°4¸ÀUÓKˆFØÔ2Ñ2ØØ�L‰L˜±™f Vœn¸VÀZÐ<PÔQØÐ!Ø×8Ñ8¸Õ>ð "r+   Nr  )rÎ   rÒ   rU  r  rf   râ   s   ````` r)   rT  z.SessionSessionsMixin._write_model_config_patch¶  s   ü€ ÷	?ð 	?ð 	×Ñ˜CÕ r+   Úskip)Ú
on_missingr\  c                ó,  — |j                  d|f«      j                  «       }|€|dk(  rt        d|› �«      ‚t        S t	        |d   «      }|j                  «       D ]  \  }}|€|j                  |d«       Œ|||<   Œ! |rt        j                  |«      S dS )u#  SELECT + tolerant-parse + merge ``patch`` into model_config (the one place that keeps
        ``_branched_from``/``_delegate_from`` alive); ``None`` deletes a key. Returns serialized JSON
        (``None`` when empty) or ``_MODEL_CONFIG_ROW_MISSING`` (``on_missing="raise"`` â†’ ValueError).ú.SELECT model_config FROM sessions WHERE id = ?NÚraisezSession not found: r   )	rq   r  Ú
ValueErrorrZ  r:   r0  Úpopr5   rÞ   )	rÎ   rt   rÒ   rU  r\  r!   ÚconfigrC   r8  s	            r)   rY  z-SessionSessionsMixin._merge_model_config_jsonÆ  s¡   € ð �l‰lÐKÈjÈ]Ó[×dÑdÓfˆØˆ;Ø˜WÒ$Ü Ð#6°z°lÐ!CÓDÐDÜ,Ð,Ü$ S¨¡VÓ,ˆØŸ+™+›-ò 	$‰JˆC�Øˆ}Ø—
‘
˜3 Õ%à#��s’ð		$ñ
 &,Œt�z‰z˜&Ó!Ð5°Ð5r+   c                 ó2   — |r|sy| j                  ||«       y)ztMerge ``patch`` into model_config atomically (``None`` removes a key);
        no-op when the row or patch is empty.N)rT  )rÎ   rÒ   rU  s      r)   Úpatch_session_model_configz/SessionSessionsMixin.patch_session_model_configÙ  s   € ñ ¡ØØ×&Ñ& z°5Õ9r+   rC   rÁ   c                 ó€   — | j                  |«      xs i }t        |j                  d«      «      j                  ||«      S )zCRead one key out of a session's model_config JSON (tolerant parse).r·   )Úget_sessionr:   r'   )rÎ   rÒ   rC   rÁ   Úsessions        r)   Úget_session_model_config_valuez3SessionSessionsMixin.get_session_model_config_valueà  s8   € à×"Ñ" :Ó.Ò4°"ˆÜ" 7§;¡;¨~Ó#>Ó?×CÑCÀCÈÓQÐQr+   )rØ   rN  Úmodel_optionsÚroute_sourceÚ	confirmedri  rj  rk  c                ó¤   ‡‡— |xs d‰xs d|xs i |xs dt        |«      t        j                  «       dœ}| j                  ‰d|idˆˆfd„«       y)z‘Persist a Browser / API-client runtime lock into model_config (lineage markers survive); null
        system_prompt so cached footers cannot lie.r%   )rN  rØ   ri  rj  rk  Ú
updated_atrP  zÝUPDATE sessions SET
                   model_config = ?,
                   model = COALESCE(?, model),
                   system_prompt = NULL,
                   system_prompt_hash = NULL
                   WHERE id = ?c                 ó   •— | ‰‰fS r°   r_   rR  s    €€r)   r  zBSessionSessionsMixin.update_session_runtime_lock.<locals>.<lambda>ø  s   ø€ ˜F E¨:Ð6€ r+   N)r  rß   rT  )rÎ   rÒ   rØ   rN  ri  rj  rk  Úlocks    ``     r)   Úupdate_session_runtime_lockz0SessionSessionsMixin.update_session_runtime_lockå  s`   ù€ ð !š B°²¸"È}ÒObÐ`bØ(Ò.¨B¼TÀ)»_Ô\`×\eÑ\eÓ\gñ
ˆð 	×&Ñ&ØÐ-¨tÐ4ð#ô 7õ		
r+   Úenabledc                 óD   — |sy| j                  |dt        |«      i«       y)zZPersist the per-session YOLO flag so ``/yolo`` survives ``--resume``; no-op without a row.NÚ	yolo_mode)rT  r  )rÎ   rÒ   rq  s      r)   Úset_session_yoloz%SessionSessionsMixin.set_session_yoloû  s!   € áØØ×&Ñ& z°KÄÀgÃÐ3OÕPr+   Úsession_metac                 ón   — t        t        | xs i j                  d«      «      j                  d«      «      S )zVPersisted YOLO flag; False on any parse failure (resume must never enable the bypass).r·   rs  )r  r:   r'   )ru  s    r)   Úsession_yolo_enabledz)SessionSessionsMixin.session_yolo_enabled  s1   € ô Ô'¨Ò);¸×(@Ñ(@ÀÓ(PÓQ×UÑUÐVaÓbÓcÐcr+   c                 ór   — | j                  «        | j                  d|f«      }|r| j                  |«      S dS )zXGet a session by ID (drains queued token deltas first so cost readers see exact totals).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.id = ?N)rE  r=  Ú_session_row_dictrA  s      r)   rf  z SessionSessionsMixin.get_session  sA   € à×ÑÔ!Ø�n‰nðkàˆMó
ˆñ
 /2ˆt×%Ñ% cÓ*Ð;°tÐ;r+   c                 óf   — | j                  «        | j                  d|f«      }|rt        |«      S dS )a'  Most recently used main-loop model route as one coherent per-call tuple
        (``session_model_usage`` keeps model+provider together; ``sessions`` mixes route changes).
        Recency, not lifetime call count: on a long session a route retired weeks ago can hold the
        highest ``api_call_count`` forever, and /status and /usage would keep calling it current.
        ``rowid DESC`` breaks same-timestamp ties toward the route that first appeared later; without
        it SQLite's temp-sort order is unspecified and the retired route can win.au  SELECT model, billing_provider, billing_base_url, billing_mode,
                      api_call_count
                 FROM session_model_usage
                WHERE session_id = ?
                  AND task = ''
                  AND model <> 'unknown'
                  AND billing_provider <> ''
                ORDER BY last_seen DESC, rowid DESC
                LIMIT 1N)rE  r=  r9   rA  s      r)   Úget_recent_session_model_routez3SessionSessionsMixin.get_recent_session_model_route  s=   € ð 	×ÑÔ!Ø�n‰nðð ˆMó
ˆñ  Œt�C‹yÐ) TÐ)r+   Úsession_id_or_prefixc                 ó    — | j                  |«      }|r|d   S | j                  dt        |«      › d�f«      }t        |«      dk(  r|d   d   S dS )z>Exact id, else the single unambiguous prefix match, else None.ro   zSSELECT id FROM sessions WHERE id LIKE ? ESCAPE '\' ORDER BY started_at DESC LIMIT 2ú%rð   r   N)rf  rö   r?   r¬   )rÎ   r|  rù   Úmatchess       r)   Úresolve_session_idz'SessionSessionsMixin.resolve_session_id&  sf   € à× Ñ Ð!5Ó6ˆÙØ˜‘;ÐØ—.‘.ØbÜÐ1Ó2Ð3°1Ð5Ð7ó
ˆô $' w£<°1Ò#4ˆw�q‰z˜$ÑÐ>¸$Ð>r+   c                 óp   — |xs dj                  «       }|syt        | j                  d|f«      xs d«      S )ui  Stamp this store's own profile onto legacy ``profile_name IS NULL`` rows, which the fail-closed
        owner ladder cannot route. Never overwrites a non-NULL owner. Returns rows stamped.

        Sessions created before the durable-ownership work (#95407 lineage) carry ``profile_name = NULL``.
        On single-backend installs that was harmless, but once a Desktop registers a second connection the
        fail-closed owner ladder (which is correct for new sessions) can no longer route those rows anywhere
        â€” every pre-campaign session becomes unresumable after upgrade (#94724, field report).
        r%   r   zvUPDATE sessions
               SET profile_name = ?
             WHERE profile_name IS NULL OR TRIM(profile_name) = '')r(   r  r)  )rÎ   r¤   Ústamps      r)   Úbackfill_null_session_profilesz3SessionSessionsMixin.backfill_null_session_profiles1  sN   € ð Ò# ×*Ñ*Ó,ˆÙØÜ�4×'Ñ'ðFð ˆHó	
ò ð
 óð 	r+   c                 ó@   — t        | j                  dd«      xs d«      S )u¼  Promote ``model_config.cwd`` into the cwd column for ACP rows lacking one.

        ACP sessions minted before the adapter populated the column still carry
        their workspace inside ``model_config``, written by the same adapter that
        knew the real directory â€” so this is a record being promoted, not a guess.
        Only fills NULL/empty; an explicit column value always wins. Returns the
        number of rows changed.
        a  UPDATE sessions
                  SET cwd = json_extract(model_config, '$.cwd')
                WHERE source = 'acp'
                  AND COALESCE(cwd, '') = ''
                  AND json_valid(model_config)
                  AND COALESCE(json_extract(model_config, '$.cwd'), '') != ''r_   r   )r  r)  ©rÎ   s    r)   Úbackfill_acp_session_cwdz-SessionSessionsMixin.backfill_acp_session_cwdD  s3   € ô �4×'Ñ'ðQð ó
ò ð óð 	r+   Úcolumnr8  c                 ó:   — | j                  d|› d�|||f«      dkD  S )z¹Set one ``sessions`` column across a whole compression lineage: Desktop projects roots
        forward to their tip, so updating only the tip would let the root resurrect it on refresh.a¡  
            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 parent.end_reason = 'compression'
              ),
              descendants(id) AS (
                SELECT ?
                UNION
                SELECT child.id
                FROM descendants d
                JOIN sessions parent ON parent.id = d.id
                JOIN sessions child ON child.parent_session_id = parent.id
                WHERE parent.end_reason = 'compression'
              ),
              lineage(id) AS (
                SELECT id FROM ancestors
                UNION
                SELECT id FROM descendants
              )
            UPDATE sessions
            SET zB = ?
            WHERE id IN (SELECT id FROM lineage)
            r   )r)  )rÎ   r‡  rÒ   r8  s       r)   Ú_set_lineage_columnz(SessionSessionsMixin._set_lineage_columnW  sC   € ð ×#Ñ#ðð4 �ð ð5ð: ˜ UÐ+ó=
ð> ñ?ð 	r+   Úarchivedc                 ó:   — | j                  d|t        |«      «      S )zOSoft-hide (or unhide) a session and its compression lineage; messages are kept.rŠ  ©r‰  r  )rÎ   rÒ   rŠ  s      r)   Úset_session_archivedz)SessionSessionsMixin.set_session_archived{  s   € à×'Ñ'¨
°JÄÀHÃÓNÐNr+   c                 óŒ  — |sy	 | j                  |«      }|r|j                  d«      sy|}	 | j                  |«      xs |}||k7  r| j                  |«      xs |}|j                  d«      xs d| j                  vry| j                  |d«      sy| j                  d|d   f«       y# t        $ r Y yw xY w# t        $ r Y Œfw xY w)u>  Un-archive a session archived by a recoverable accident (ws_orphan_reap, agent_close);
        deliberate archives are left alone. True when un-archived.

        Registry-style lookups (Bot Mode's canonical "Bot Chat") use this to resurrect a row the ws-orphan
        reaper (``ws_orphan_reap``) or older agent cleanup (``agent_close``) archived: those ends are
        accidents, not user intent, so the identity-scoped canonical chat must survive them (#92687).
        Sessions archived with no end_reason or an explicit boundary reason (user archived deliberately,
        ``session_reset``, â€¦) are left untouched â€” returns ``False`` for those, ``True`` only when the row
        was archived for a recoverable reason and is now un-archived (whole compression lineage, via
        :meth:`set_session_archived`).
        FrŠ  rÿ   r%   r  ro   T)rf  rË   r'   Úget_compression_tipÚRECOVERABLE_END_REASONSr�  rñ   )rÎ   rÒ   r!   ÚtipÚtip_ids        r)   Úunarchive_recoverable_sessionz2SessionSessionsMixin.unarchive_recoverable_sessionƒ  së   € ñ Øð	Ø×"Ñ" :Ó.ˆCñ ˜#Ÿ'™' *Ô-Øàˆð	Ø×-Ñ-¨jÓ9ÒG¸ZˆFØ˜Ò#Ø×&Ñ& vÓ.Ò5°#�ð �G‰G�LÓ!Ò' R°×0LÑ0LÑLØØ×(Ñ(¨°UÔ;Øð 	�‰ØQÐTWÐX\ÑT]ÐS_ô	
ð øô+ ò 	Ùð	ûô ò 	Ùð	ús"   …B( ­/B7 Â(	B4Â3B4Â7	CÃCÚpinnedc                 ó$  — | j                  d|t        |«      «      }|rq| j                  |«      }t        |«      xr> t        |j	                  d«      «      xr" |j	                  d«      xs d| j
                  k(  }|s| j                  d|d«       |S )aG  Pin/unpin a session and its compression lineage (pins are exempt from the auto_archive sweep).
        Pinning also clears ``hidden``: a pin means "keep this visible", and a hidden+pinned row is
        otherwise absent from both the default listing and the pinned back-fill (see #106171).
        Exempt the canonical Bot Chat (hidden + exact registry title): the desktop contract keeps it
        hidden and reachable only through the bot row, and unhiding it would also disable the
        rename guard in ``_set_session_title`` that protects its identity (see review on #106180).r”  ÚhiddenÚtitler%   r   )r‰  r  rf  r  r'   ÚCANONICAL_BOT_CHAT_TITLE)rÎ   rÒ   r”  Úresultr!   Úis_canonical_bot_chats         r)   Úset_session_pinnedz'SessionSessionsMixin.set_session_pinnedª  s�   € ð ×)Ñ)¨(°JÄÀFÃÓLˆÙØ×"Ñ" :Ó.ˆCÜ$(¨£Iò %´$°s·w±w¸xÓ7HÓ2Iò %Ø—‘˜Ó!Ò' R¨D×,IÑ,IÑIð "ñ )Ø×(Ñ(¨°:¸qÔAØˆr+   r–  c                 ó:   — | j                  d|t        |«      «      S )z\Hide/unhide a session and its compression lineage from the default listing; still resumable.r–  rŒ  )rÎ   rÒ   r–  s      r)   Úset_session_hiddenz'SessionSessionsMixin.set_session_hidden»  s   € à×'Ñ'¨°*¼cÀ&»kÓJÐJr+   Úreadc                 ó\   — | j                  d||rt        j                  «       «      S d«      S )zÄMark read/unread across the compression lineage. ``last_read_at`` is a watermark: unread when
        activity postdates it (no write on the message path). NULL = never tracked = read; 0 = unread.Úlast_read_atç        )r‰  rß   )rÎ   rÒ   rž  s      r)   Úset_session_readz%SessionSessionsMixin.set_session_read¿  s+   € ð ×'Ñ'¨¸
ÑSWÄDÇIÁIÃKÓaÐaÐ]`ÓaÐar+   Úsession_rowc                 ó¨   — | j                  d«      }|€y| j                  d«      xs | j                  d«      }t        |xs d«      t        |«      kD  S )zIUnread = activity postdates the ``last_read_at`` watermark (NULL = read).r   FÚlast_activeÚ
started_atr   )r'   r7  )r£  Ú	last_readr¥  s      r)   Úsession_unreadz#SessionSessionsMixin.session_unreadÄ  sQ   € ð  —O‘O NÓ3ˆ	ØÐØØ!—o‘o mÓ4ÒU¸¿¹ÈÓ8UˆÜ�[Ò% AÓ&¬¨yÓ)9Ñ9Ð9r+   >   rÙ   rá   Úgit_metadata_generationÚ_session_compact_cols_sqlÚ	where_sqlÚ	id_needleÚsearch_needlec                 ó   — g }g }dt         dt         fd„}|r(|j                  d«       |j                   ||«      «       |rft        j                  dd|«      }d}|j	                   ||«      gdz  «       |r|d	z  }|j                   ||«      «       |j                  |d
z   «       |s| |fS dj                  |«      }| r	| › d|› �|fS d|› �|fS )a  Extend ``where_sql`` with the id_query / search_query filters: a row is admitted when its own
        id or any id in its forward compression chain matches (search also matches titles and a
        punctuation-stripped form so ``an94`` finds ``AN-94``); chain membership bounds the LIKE.Úneedler"   c                 ó    — dt        | «      › d�S )Nr~  )r?   )r¯  s    r)   Úlikez6SessionSessionsMixin._chain_search_where.<locals>.likeÚ  s   € Ø”| FÓ+Ð,¨AÐ.Ð.r+   znEXISTS (SELECT 1 FROM chain cq        WHERE cq.root_id = s.id          AND LOWER(cq.cur_id) LIKE ? ESCAPE '\')z[\W_]+r%   z·EXISTS (SELECT 1 FROM chain cq JOIN sessions cs ON cs.id = cq.cur_id WHERE cq.root_id = s.id AND (LOWER(COALESCE(cs.title, '')) LIKE ? ESCAPE '\' OR LOWER(cq.cur_id) LIKE ? ESCAPE '\'rm   z{ OR REPLACE(REPLACE(REPLACE(REPLACE(LOWER(COALESCE(cs.title, '')), '-', ''), '_', ''), '.', ''), ' ', '') LIKE ? ESCAPE '\'rE   rM   rL   )r4   rc   rÈ   Úsubrd   rN   )	r«  r¬  r­  rf   rI   r±  Úcompact_needleÚsearch_clauseÚcombineds	            r)   Ú_chain_search_wherez(SessionSessionsMixin._chain_search_whereÓ  s   € ð
 ˆØˆð	/œð 	/¤ó 	/áØ�N‰NðEôð �M‰M™$˜y›/Ô*ÙÜŸV™V I¨r°=ÓAˆNð:ð ð
 �M‰M™4 Ó.Ð/°!Ñ3Ô4ÙØðQñ�ð —‘™d >Ó2Ô3Ø�N‰N˜=¨4Ñ/Ô0ÙØ˜fÐ$Ð$Ø—<‘< Ó(ˆÙ1:�9�+˜U 8 *Ð-ÐV\Ð\Ð\À&ÈÈ
Ð@SÐV\Ð\Ð\r+   ÚsessionsÚcompact_rowsc                 ód  — i }|D ]B  }|j                  d«      dk(  sŒ| j                  |d   «      }|sŒ/|d   |d   k7  sŒ;|||d   <   ŒD |r2| j                  |j                  «       D �ch c]  }|d   ’Œ	 c}|¬«      ni }g }|D ]¤  }|j                  |d   «      }|r|j                  |d   «      nd}|s|j	                  |«       ŒCt        |«      }	dD ]  }
|
|v sŒ||
   |	|
<   Œ |	j                  d«      €|j                  d«      |	d<   |d   |	d	<   ||	d
<   |j	                  |	«       Œ¦ |S c c}w )zçReplace each compression root's surfaced fields with its live tip's (root ``started_at`` kept
        for stable ordering), one batched query. ``_lineage_ids`` carries every chain id (a tile may
        hold a MIDDLE segment's id).rÿ   Úcompressionro   éÿÿÿÿ)r¸  N)ro   Úended_atrÿ   Úmessage_countÚtool_call_countr—  r¥  ÚpreviewrØ   rÙ   r&   rµ   r$   r—  Ú_lineage_root_idÚ_lineage_ids)r'   Úget_compression_chainÚ_get_session_rich_rows_batchri   rc   r9   )rÎ   r·  r¸  Úchain_by_rootÚsÚchainÚtip_rowsÚ	projectedÚtip_rowrS  rC   s              r)   Ú_project_compression_tipsz.SessionSessionsMixin._project_compression_tipsö  sy  € ð /1ˆØò 	3ˆAØ�u‰u�\Ó" mÓ3Ø×2Ñ2°1°T±7Ó;�Ú˜U 2™Y¨!¨D©'Ó1Ø-2�M ! D¡'Ò*ð		3ñ ð ×-Ñ-Ø(5×(<Ñ(<Ó(>Ö?˜u��r“Ò?Èlð .ô à$&ð 	ð
 ˆ	Øò 	%ˆAØ!×%Ñ% a¨¡gÓ.ˆEÙ16�h—l‘l 5¨¡9Ô-¸DˆGÙØ× Ñ  Ô#ØÜ˜!“WˆFðò /�ð ˜'’>Ø")¨#¡,�F˜3’Kð/ð �z‰z˜'Ó"Ð*ð #$§%¡%¨£.��w‘Ø)*¨4©ˆFÐ%Ñ&Ø%*ˆF�>Ñ"Ø×Ñ˜VÕ$ð)	%ð* Ðùò3 @s   Á)D-é   g      @)ÚlimitrU   Útimeout_secondsÚcandidate_limitÚlineage_limitrÌ  rU   rÍ  rÎ  rÏ  c                ó4  ‡‡— t        dt        |«      «      }t        dt        |«      «      }|€t        d|dz  «      }t        |t        t        |«      d«      «      }|€t        d|dz  «      }t        |t        t        |«      d«      «      }dd	t	        d
«      › d�g}g }|r>dj                  d„ |D «       «      }|j                  d|› d�«       |j                  |«       dj                  |«      }	dt	        d«      › d�}
d|	› d|
› d|
› d|
› dt        d«      › d|
› dt        › dt        › dt        › dt	        d
«      › d�}|||||gz   }t        j                  «       |z   Šd Šd!t        fˆˆfd"„}	 | j                  «       5 }|j                  |d#«       	 |j!                  ||«      j#                  «       }|j                  dd$«       	 ddd«       g }D ]V  }| j/                  |«      }t1        |j3                  d)d*«      «      |d+<   | j5                  |«      |d,<   |j                  |«       ŒX |S # |j                  dd$«       w xY w# 1 sw Y   ŒxY w# t$        j&                  $ r4}‰r,d%t)        |«      j+                  «       v rt-        d&|d'›d(�«      |‚‚ d}~ww xY w)-a,  Latency-bounded recent-conversation browse (``session_search()``): preselect a small candidate set
        from the indexed durable activity timestamp (fallback ``started_at``), resolve only those across
        compression ancestry/chains, then hydrate activity/previews for that bounded set. Lineage traversal
        uses ``UNION`` plus a total-row ceiling so a corrupt cycle or a deep/branching lineage cannot defeat
        the bound; a lineage that hits the ceiling before a terminal root/tip is omitted, not expanded.
        A cooperative SQLite progress deadline interrupts sustained work past ``timeout_seconds`` and raises
        ``TimeoutError`` (cheap statements may finish between callbacks). Supports only the agent-tool
        browse filters; rich callers keep using :meth:`list_sessions_rich`.rð   r¡  Né€   é   i   i    r`   ús.hidden = 0rZ   r[   ú,c              3   ó    K  — | ]  }d –— Œ y­w)ú?Nr_   )rž   r(  s     r)   rŸ   zDSessionSessionsMixin.list_recent_sessions_bounded.<locals>.<genexpr>>  s   è ø€ Ò#A¨A¤CÑ#Aùs   ‚r^   r]   rM   zî
            parent.end_reason = 'compression'
            AND child.parent_session_id = parent.id
            AND json_extract(
                COALESCE(child.model_config, '{}'), '$._branched_from'
            ) IS NULL
            AND r  zF IS NULL
            AND COALESCE(child.source, '') != 'tool'
        z•
            WITH RECURSIVE
            recent_candidates(id) AS (
                SELECT s.id
                FROM sessions s
                WHERE aÙ  
                ORDER BY COALESCE(s.last_activity_at, s.started_at) DESC,
                         s.started_at DESC, s.id DESC
                LIMIT ?
            ),
            ancestors(candidate_id, cur_id) AS (
                SELECT id, id FROM recent_candidates
                UNION
                SELECT a.candidate_id, parent.id
                FROM ancestors a
                JOIN sessions child ON child.id = a.cur_id
                JOIN sessions parent ON aZ  
                LIMIT ?
            ),
            candidate_roots(root_id) AS (
                SELECT DISTINCT a.cur_id
                FROM ancestors a
                JOIN sessions child ON child.id = a.cur_id
                WHERE NOT EXISTS (
                    SELECT 1
                    FROM sessions parent
                    WHERE aI  
                )
            ),
            chain(root_id, cur_id) AS (
                SELECT root_id, root_id FROM candidate_roots
                UNION
                SELECT c.root_id, child.id
                FROM chain c
                JOIN sessions parent ON parent.id = c.cur_id
                JOIN sessions child ON z¬
                LIMIT ?
            ),
            chain_rows AS (
                SELECT
                    c.root_id,
                    c.cur_id,
                    zc.cur_idz² AS activity,
                    CASE WHEN EXISTS (
                        SELECT 1
                        FROM sessions parent
                        JOIN sessions child ON a   
                        WHERE parent.id = c.cur_id
                    ) THEN 0 ELSE 1 END AS is_tip
                FROM chain c
            ),
            ranked_tips AS (
                SELECT root_id, cur_id, activity,
                       ROW_NUMBER() OVER (
                           PARTITION BY root_id
                           ORDER BY activity DESC, cur_id DESC
                       ) AS rank_in_root
                FROM chain_rows
                WHERE is_tip = 1
            )
            SELECT
                tip.id,
                tip.source,
                tip.model,
                COALESCE(tip.title, s.title) AS title,
                s.started_at AS started_at,
                tip.ended_at,
                tip.end_reason,
                tip.message_count,
                tip.tool_call_count,
                rt.activity AS last_active,
                COALESCE(
                    (SELECT zÎ
                     FROM messages m
                     WHERE m.session_id = tip.id
                       AND m.role = 'user'
                       AND m.content IS NOT NULL
                       AND aÛ  
                     ORDER BY m.timestamp, m.id LIMIT 1),
                    ''
                ) AS _preview_raw,
                CASE WHEN s.id != tip.id THEN s.id ELSE NULL END
                    AS _lineage_root_id
            FROM ranked_tips rt
            JOIN sessions s ON s.id = rt.root_id
            JOIN sessions tip ON tip.id = rt.cur_id
            WHERE rt.rank_in_root = 1
              AND s.archived = 0
              AND s.hidden = 0
              AND z
              AND zk IS NULL
            ORDER BY rt.activity DESC, s.started_at DESC, tip.id DESC
            LIMIT ?
        Fr"   c                  ó:   •— t        j                  «       ‰ k\  rdŠyy)NTrð   r   )rß   Ú	monotonic)ÚdeadlineÚinterrupted_by_deadlines   €€r)   Ú_deadline_progress_handlerzUSessionSessionsMixin.list_recent_sessions_bounded.<locals>._deadline_progress_handler¹  s   ø€ ä�~‰~Ó 8Ò+Ø*.Ð'ØØr+   iè  r   Ú	interruptzrecent-session browse exceeded Úgz
s deadlineÚ_preview_rawr%   r¿  Úunread)Úmaxr  r7  Úminr0   rN   rc   rd   r   r   r   r   rß   rØ  Ú	_read_ctxÚset_progress_handlerrq   rr   r>  ÚOperationalErrorr4   r”   ÚTimeoutErrorry  r   ra  r¨  )rÎ   rÌ  rU   rÍ  rÎ  rÏ  Úcandidate_clausesÚcandidate_paramsÚplaceholdersÚcandidate_whereÚcompression_parent_edger÷   rf   rÛ  rt   rø   Úexcr·  r!   rg  rÙ  rÚ  s                       @@r)   Úlist_recent_sessions_boundedz1SessionSessionsMixin.list_recent_sessions_bounded  s,  ù€ ô" �A”s˜5“zÓ"ˆÜ˜c¤5¨Ó#9Ó:ˆØÐ"Ü! # u¨q¡yÓ1ˆOÜ˜e¤S¬¨_Ó)=¸tÓ%DÓEˆØÐ Ü  o¸Ñ&9Ó:ˆMÜ˜O¬S´°]Ó1CÀTÓ-JÓKˆð ØÜ"Ð#3Ó4Ð5°XÐ>ð
Ðð
 ')ÐÙØŸ8™8Ñ#A°Ô#AÓAˆLØ×$Ñ$Ð'8¸¸ÀaÐ%HÔIØ×#Ñ# OÔ4Ø!Ÿ,™,Ð'8Ó9ˆð'ô %Ð%9Ó:Ð;ð <	ð#Ððð
 'Ð'ð ()ð *AÐ(Að 
Bð 3Ð3ð 	4(ð )@Ð'@ð Aô 4°JÓ?Ð@ð A0ð 1HÐ/Hð Iô4 1Ð1ð 2ô
 2Ð2ð 3ô 'Ð'ð (Ü&Ð'7Ó8Ð9ð :	ðu]ˆð| "ØØØØð	%
ñ 
ˆô —>‘>Ó# oÑ5ˆØ"'Ðð	¬Cö 	ð	Ø—‘Ó!ð 7 TØ×)Ñ)Ð*DÀdÔKð7ØŸ<™<¨¨vÓ6×?Ñ?ÓA�Dà×-Ñ-¨d°AÕ6÷7ð ˆØò 	%ˆCØ×,Ñ,¨SÓ1ˆGÜ!/°·±¸NÈBÓ0OÓ!PˆG�IÑØ $× 3Ñ 3°GÓ <ˆG�HÑØ�O‰O˜GÕ$ð		%ð
 ˆøð ×-Ñ-¨d°AÕ6ú÷7ð 7ûô ×'Ñ'ò 	Ù&¨;¼#¸c»(¿.¹.Ó:JÑ+JÜ"Ø5°oÀaÐ5HÈ
ÐSóàðð ûð	úsH   Å/I Å?IÆ H-Æ3IÇI È-IÉIÉIÉ	I ÉJÉ#/JÊJr!   c                 óˆ   — | j                  |«      }t        |j                  dd«      «      |d<   |j                  dd«       |S )zTProject a list_sessions_rich row: shape the preview, drop internal ordering columns.rÞ  r%   r¿  Ú_effective_last_activeN)ry  r   ra  )Úclsr!   rÅ  s      r)   Ú	_list_rowzSessionSessionsMixin._list_rowÖ  sA   € ð ×!Ñ! #Ó&ˆÜ% a§e¡e¨N¸BÓ&?Ó@ˆˆ)‰Ø	�‰Ð&¨Ô-Øˆr+   rS   r;   ÚoffsetÚinclude_childrenrV   Úproject_compression_tipsÚorder_by_last_activerX   rW   Úid_queryÚsearch_queryÚinclude_pinnedÚinclude_hiddenc                 óÆ  — | j                  «        t        | ||||||||¬«	      \  }}|s|s|j                  d«       t        |«      }t	        |«      }d|r| j                  «       nd› �|rdndz   dt        › d�z   }|rdnd}d	|› �}|
r»| j                  ||xs dj                  «       j                  «       |xs dj                  «       j                  «       «      \  }}d
|› dt        dd«      › dt        dd«      › dt        j                  d¬«      › dt        d«      › d|› t        d«      › d|› d|› d�}||z   |z   ||gz   }n*d|› t        d«      › d|› d|› d�}|j                  ||g«       | j!                  ||«      D �cg c]  }| j#                  |«      ‘Œ }}|r‡|D �ch c]  }|d   ’Œ	 } }|r|› d�nd}!d|› t        d«      › d|› d|!› d�}"| j!                  |"|«      D ]@  }| j#                  |«      }|d   | vsŒ| j%                  |d   «       |j                  |«       ŒB |	r|s| j'                  ||«      }|D ]  }| j)                  |«      |d <   Œ |S c c}w c c}w )!a   List sessions with preview and ``last_active`` in one query. ``order_by_last_active`` sorts
        by the chain TIP via a recursive CTE (the only path honouring ``id_query`` / ``search_query``);
        ``include_pinned`` back-fills pins the page missed, still obeying the other filters.rP   rÓ  zSELECT zs.*r%   zA, COALESCE(sp.prompt, s.system_prompt) AS _system_prompt_resolvedz,
                    z=LEFT JOIN system_prompts sp ON sp.hash = s.system_prompt_hashz FROM sessions s
                zr
                WITH RECURSIVE chain(root_id, cur_id) AS (
                    SELECT s.id, s.id FROM sessions s aT  
                    UNION ALL
                    SELECT c.root_id, child.id
                    FROM chain c
                    JOIN sessions parent ON parent.id = c.cur_id
                    JOIN sessions child ON child.parent_session_id = c.cur_id
                    WHERE parent.end_reason = 'compression'
                      AND r  rý   z# IS NULL
                      AND r.   z( IS NULL
                      AND NOT (r  )ÚazË)
                      AND COALESCE(child.source, '') != 'tool'
                ),
                chain_max AS (
                    SELECT
                        root_id,
                        MAX(Úcur_idz�) AS effective_last_active
                    FROM chain
                    GROUP BY root_id
                )
                rÅ  zÜ AS last_active,
                    COALESCE(cm.effective_last_active, s.started_at) AS _effective_last_active
                FROM sessions s
                LEFT JOIN chain_max cm ON cm.root_id = s.id
                z
                z�
                ORDER BY _effective_last_active DESC, s.started_at DESC, s.id DESC
                LIMIT ? OFFSET ?
            z  AS last_active
                zY
                ORDER BY s.started_at DESC
                LIMIT ? OFFSET ?
            ro   z AND s.pinned = 1zWHERE s.pinned = 1z8
                ORDER BY s.started_at DESC
            rß  )rE  rj   rc   rO   ra   Ú_compact_session_colsÚ_PREVIEW_COL_SQLr¶  r(   r”   r   r   Úformatr   r   rd   rö   rð  rs   rÊ  r¨  )#rÎ   rR   rS   rU   r;   rÌ  rñ  rò  rV   ró  rô  rX   rW   rõ  rö  r¸  r÷  rT   rø  Úwhere_clausesrf   r«  Úbase_where_paramsÚselect_headÚprompt_joinÚfrom_sessionsÚouter_whereÚ	id_paramsr÷   r!   r·  rÅ  Úseen_idsÚpinned_whereÚpinned_querys#                                      r)   Úlist_sessions_richz'SessionSessionsMixin.list_sessions_richÞ  s4  € ð 	×ÑÔ!Ü 5Ø!1Ð1¸&È'Ð_jØ+¸
ÐVgØ'Ð:Jô!
Ñˆ�vñ ¡mØ× Ñ  Ô0Ü˜}Ó-ˆ	Ü  ›LÐð ±l�d×0Ñ0Ô2ÈÐNÐOÙ!‰rÐ'jñlà'Ô(8Ð'9Ð9PÐQñRð 	ñ ‰BÐ$cð 	ð <¸K¸=ÐIˆÙð
 &*×%=Ñ%=Ø˜HšN¨×1Ñ1Ó3×9Ñ9Ó;¸lÒ>PÈb×=WÑ=WÓ=Y×=_Ñ=_Ó=aó&Ñ"ˆK˜ð7à7@°kð Bô -Ð-AÐCUÓVÐWð XÜ,Ð-AÐCUÓVÐWð X Ü 0× 7Ñ 7¸'Ô BÐCð Dô <¸HÓEÐFð Gð �Ô6°sÓ;Ð<ð =ð �ð Ø�ð ð5ˆEð< ˜f‘_ yÑ0°E¸6°?ÑB‰FðØ�Ô6°sÓ;Ð<ð =Ø�ð  Ø�ð ðˆEð �M‰M˜5 &˜/Ô*Ø37·>±>À%ÈÓ3PÖQ¨C�D—N‘N 3Õ'ÐQˆÐQñ Ø)1Ö2 A˜˜$›Ð2ˆHÐ2Ù>G˜i˜[Ð(9Ñ:ÐMaˆLð Ø�Ô6°sÓ;Ð<ð =Ø�ð  Ø�ð ðˆLð —~‘~ lÐ4EÓFò '�Ø—N‘N 3Ó'�Ø�T‘7 (Ò*Ø—L‘L  4¡Ô)Ø—O‘O AÕ&ð	'ñ
 $Ñ,<Ø×5Ñ5°hÀÓMˆHàò 	1ˆAØ×-Ñ-¨aÓ0ˆAˆhŠKð	1àˆùò- Rùò 3s   ÆIÆ#IÚsession_idsc                 ó  — |xs g D �cg c]  }|sŒ|‘Œ	 }}|si S |D �ci c]  }|d“Œ }}| j                  dt        |«      › d�|«      }|D ](  }t        |d   t        |d   «      |d   ¬«      ||d   <   Œ* |S c c}w c c}w )	u¦   ``{session_id: status}`` from each session's LAST message row (``'empty'`` when none); one
        query, MAX(id) per session joined back â€” never scans transcripts.r‹   a3  
            SELECT m.session_id, m.role,
                   m.tool_calls IS NOT NULL AS has_tool_calls,
                   m.finish_reason
            FROM messages m
            JOIN (
                SELECT session_id, MAX(id) AS max_id
                FROM messages
                WHERE session_id IN (z[)
                GROUP BY session_id
            ) latest ON m.id = latest.max_id
        rŽ   r�   r�   )rŽ   r�   r�   rÒ   )rö   rb   rš   r  )rÎ   r
  rv   rƒ   Ústatusesrø   r!   s          r)   Úsession_lifecycle_statusesz/SessionSessionsMixin.session_lifecycle_statusesI  sÁ   € ð  +Ò0¨bÖ9�s²SŠsÐ9ˆÐ9ÙØˆIØ<?Ö#@°S C¨¡LÐ#@ˆÐ#@Ø�~‰~ð #&ô '@ÀÓ&DÐ%Eð F	ðð óˆð ò 	ˆCÜ*AØ˜‘[´°cÐ:JÑ6KÓ1LØ! /Ñ2ô+ˆH�S˜Ñ&Ò'ð	ð
 ˆùò+ :ùò $As   ‰A9‘A9Ÿ
A>Úmax_messagesc                 óÄ   — ddl m}m} |€ |«       }|dk  rt        d«      ‚|dk(  ry| j	                  d||dz   f«      }t        |r|d   nd«      }||kD  r
 ||||«      ‚|S )zØActive row count of this segment, or raise SessionExportTooLargeError (the LIMITed subquery
        stops once the bound is exceeded). ``None`` resolves ``sessions.max_export_messages``; 0 disables
        the guard.r   )ÚSessionExportTooLargeErrorÚresolved_max_export_messagesz!max_messages must be non-negativezYSELECT COUNT(*) FROM (SELECT 1 FROM messages WHERE session_id = ? AND active = 1 LIMIT ?)rð   )r    r  r  r`  r=  r  )rÎ   rÒ   r  r  r  r!   r½  s          r)   Úassert_export_safez'SessionSessionsMixin.assert_export_safec  s„   € ÷ 	ZØÐÙ7Ó9ˆLØ˜!ÒÜÐ@ÓAÐAØ˜1ÒØØ�n‰nØgØ˜¨Ñ)Ð*ó
ˆô ¡c˜C šF¨qÓ1ˆØ˜<Ò'Ù,¨Z¸ÈÓUÐUØÐr+   c                 ó†   — |sy| j                  d|f«      }|duxr& t        t        |d   «      j                  d«      «      S )z”Copied user-facing branch (``_branched_from``)? Branches own a copied transcript;
        compression continuations need the parent's archived rows.Fr^  Nr   rº   )r=  r  r:   r'   rA  s      r)   Ú_is_explicit_branch_sessionz0SessionSessionsMixin._is_explicit_branch_sessionw  sH   € ñ ØØ�n‰nÐMÐPZÈ}Ó]ˆØ˜$ˆÒZ¤4Ô(;¸CÀ¹FÓ(C×(GÑ(GÐHXÓ(YÓ#ZÐZr+   c                 óV  — |s|gS g }|}| j                  «       5 }|ra||vr]t        |«      dk  rO|j                  |«       |j                  d|f«      j	                  «       }|€n|d   }|r||vrt        |«      dk  rŒOd d d «       t        t        |«      «      xs |gS # 1 sw Y   Œ"xY w)Néd   z3SELECT parent_session_id FROM sessions WHERE id = ?r   )râ  r¬   rc   rq   r  ra   Úreversed)rÎ   rÒ   rÆ  r  rt   r!   s         r)   Ú_session_lineage_root_to_tipz1SessionSessionsMixin._session_lineage_root_to_tip  sº   € ÙØ�<ÐØˆØˆØ�^‰^Óð 	! Ù˜g¨UÑ2´s¸5³zÀCÒ7GØ—‘˜WÔ%Ø—l‘lØIÈGÈ:óç‘(“*ð ð �;ØØ˜a™&�ñ ˜g¨UÑ2´s¸5³zÀCÓ7G÷	!ô ”H˜U“OÓ$Ò4¨¨Ð4÷	!ð 	!ús   šA#BÂB(r*   c           	      óØ  — g }g }|r_t        |t        «      r|gn
t        |«      }|j                  ddj	                  dt        |«      z  «      › d�«       |j                  |«       |r0t        |«      \  }}	|j                  |«       |j                  |	«       | j                  dt        d«      › dt        |d«      › d	�g |¢|‘|‘«      D �
cg c]  }
| j                  |
«      ‘Œ c}
S c c}
w )
zÃSessions MRU-first with a computed ``last_active``; ``workspace_key`` scopes to one workspace
        so ``hermes -c``/``--resume`` picks its last session. ``source`` may be one label or several.r\   rÔ  rÖ  r]   zMSELECT s.*, COALESCE(sp.prompt, s.system_prompt) AS _system_prompt_resolved, rÅ  z^ AS last_active FROM sessions s LEFT JOIN system_prompts sp ON sp.hash = s.system_prompt_hash rª   zI ORDER BY last_active DESC, s.started_at DESC, s.id DESC LIMIT ? OFFSET ?)r3   r4   ra   rc   rN   r¬   rd   rH   rö   r   rO   ry  )rÎ   rR   rÌ  rñ  r*   rÿ  rf   rS   Ú	ws_clauseÚ	ws_paramsr!   s              r)   Úsearch_sessionsz$SessionSessionsMixin.search_sessions�  sú   € ð ˆØˆÙÜ",¨V´SÔ"9�v‘h¼tÀF»|ˆGØ× Ñ  =°·±¸#ÄÀGÃÑ:LÓ1MÐ0NÈaÐ!PÔQØ�M‰M˜'Ô"ÙÜ#8¸Ó#GÑ ˆI�yØ× Ñ  Ô+Ø�M‰M˜)Ô$Ø7;·~±~Ø[Ü'¨Ó,Ð-ð .]ä˜-¨Ó-Ð.ð /WðWð
 %ˆfÐ$�eÐ$˜VÐ$ó8
ö °�×&Ñ& sÕ+ò ð 	ùò s   ÃC'rQ   c	           
      ór   — t        ||||||||¬«      \  }	}
| j                  dt        |	d«      › �|
«      d   S )zWCount sessions with list_sessions_rich's filters so a paired "load more" total matches.)rQ   rR   rS   rU   r;   rV   rW   rX   zSELECT COUNT(*) FROM sessions srª   r   )rj   r=  rO   )rÎ   rR   rS   r;   rV   rX   rW   rQ   rU   rÿ  rf   s              r)   Úsession_countz"SessionSessionsMixin.session_count¨  sS   € ô !6Ø-°fÀgØ+¸
ÐVgØ'Ð:Jô!
Ñˆ�vð
 �~‰~Ð ?Ä
È=ÐZ]Ó@^Ð?_Ð`ÐbhÓiÐjkÑlÐlr+   Únc                 ó@   — t        | j                  d|f«      «      |k\  S )z[At least N sessions exist (archived included); LIMIT short-circuits session_count()'s scan.zSELECT 1 FROM sessions LIMIT ?)r¬   rö   )rÎ   r  s     r)   Úsession_count_gez%SessionSessionsMixin.session_count_geµ  s    € ä�4—>‘>Ð"BÀQÀDÓIÓJÈaÑOÐOr+   )rX   rW   rQ   c                ó\  — t        |||¬«      \  }}| j                  «       5 }| j                  €t        d«      ‚|j	                  dt        |d«      › d�|«      j                  «       }ddd«       D �ci c]!  }t        |d   «      t        |d   xs d	«      “Œ# c}S # 1 sw Y   Œ6xY wc c}w )
zV``{source: count}`` via one GROUP BY; ``exclude_children`` mirrors listing visibility.)rQ   rW   rX   NzSessionDB connection is closedzYSELECT COALESCE(NULLIF(s.source, ''), 'cli') AS source, COUNT(*) AS count FROM sessions srª   zC GROUP BY COALESCE(NULLIF(s.source, ''), 'cli') ORDER BY count DESCrR   Úcountr   )	rj   râ  Ú_connÚRuntimeErrorrq   rO   rr   r4   r  )	rÎ   rX   rW   rQ   rÿ  rf   rt   rø   r!   s	            r)   Úsession_count_by_sourcez,SessionSessionsMixin.session_count_by_source¹  sÆ   € ô
 !6Ø-¸]Ø-ô!
Ñˆ�vð �^‰^Óð 	 Ø�z‰zÐ!Ü"Ð#CÓDÐDØ—<‘<ð"Ü",¨]¸CÓ"@Ð!Að BUðUàV\ó÷ ‰h‹jð	 ÷	ð GKÖK¸s”�C˜‘MÓ"¤C¨¨G©Ò(9¸Ó$:Ñ:ÒKÐK÷	ð 	üò Ls   ¢ABÁ4&B)ÂB&c                 ó¤   — | j                  |«      }|sy| j                  |«      t        |j                  d«      xs d«      j	                  «       fS )u   (is_fork_child, source) in ONE read (prompt_cache_scope needs both from the same row).
        Missing row â†’ (False, ""); DB errors propagate (fail closed).

        ``agent/prompt_cache_scope.py`` needs both to resolve a host-declared conversation scope, and both
        live on the same ``sessions`` row; asking for them separately read that row twice per resolution
        (@teknium1 on 98811). The marker rules stay here, beside :meth:`is_explicit_fork_child`, instead of
        being re-implemented by the caller. See #98811.
        )Fr%   rR   r%   )rf  Ú_is_explicit_fork_child_rowr4   r'   r(   )rÎ   rÒ   rg  s      r)   Údeclared_scope_identityz,SessionSessionsMixin.declared_scope_identityÌ  sM   € ð ×"Ñ" :Ó.ˆÙØØ×/Ñ/°Ó8¼#¸g¿k¹kÈ(Ó>SÒ>YÐWYÓ:Z×:`Ñ:`Ó:bÐbÐbr+   Úsessions_dirc                 ó   — | €ydD �cg c]  }| |› |› �z  ‘Œ }}	 |j                  | j                  dt        j                  |«      › d�«      «       |D ]  }	 |j	                  d¬«       Œ yc c}w # t        $ r Y Œ+w xY w# t        $ r Y Œ8w xY w)zšRemove ``<id>.json``/``.jsonl`` and gateway ``request_dump_<id>_*.json``; OSError is swallowed
        so a filesystem hiccup never blocks a DB operation.N)z.jsonz.jsonlÚrequest_dump_z_*.jsonT)Ú
missing_ok)rd   ÚglobÚescapeÚOSErrorÚunlink)r*  rÒ   ÚsuffixÚtargetsÚps        r)   Ú_remove_session_filesz*SessionSessionsMixin._remove_session_filesÚ  s±   € ð ÐØØH[Ö\¸f�< Z L°°Ð"9Ó9Ð\ˆÐ\ð	ð �N‰N˜<×,Ñ,¨}¼T¿[¹[ÈÓ=TÐ<UÐU\Ð-]Ó^Ô_ð ò 	ˆAðØ—‘ D�Õ)ñ	ùò ]øô
 ò 	Ùð	ûô
 ò Ùðús(   ˆA-›7A2 ÁBÁ2	A>Á=A>Â	BÂBc                 óÚ   — | j                  «       5 }|j                  d|f«      j                  «       sg cddd«       S t        ||g«      }ddd«       |gt	        «      ¢S # 1 sw Y   ŒxY w)zœRows :meth:`delete_session` would remove: the session, then its recursive delegate children
        (branch/compression children are orphaned, not deleted).ú+SELECT 1 FROM sessions WHERE id = ? LIMIT 1N)râ  rq   r  r~   Úsorted)rÎ   rÒ   rt   Údelegate_idss       r)   Úget_session_delete_targetsz/SessionSessionsMixin.get_session_delete_targetsí  sx   € ð �^‰^Óð 	K Ø—<‘<Ð MÐPZÈ}Ó]×fÑfÔhØ÷	Kñ 	Kô 7°t¸j¸\ÓJˆL÷	Kð Ð2œV LÓ1Ð2Ð2÷	Kð 	Kús   ‘#A!¾A!Á!A*Úexpected_delete_idsc                 ó¦   ‡ ‡‡‡— g Š|�t        |«      ndŠˆˆˆ ˆfd„}‰ j                  |«      }‰D ]  }‰ j                  ||«       Œ t        |«      S )a   Delete a session and its messages; delegate children cascade, branch/compression children
        are orphaned. *expected_delete_ids*: proceed only if parent + delegate cascade still equals that
        set (re-walked inside the transaction on purpose: export-before-delete fails closed).Nc                 ód  •— | j                  d‰f«      j                  «       €y‰�‰‰ht        | ‰g«      £k7  ry‰j                  t	        | ‰g«      «       | j                  d‰f«       | j                  d‰f«       | j                  d‰f«       ‰j                  | «       ‰j                  ‰«       y)Nr7  FzHUPDATE sessions SET parent_session_id = NULL WHERE parent_session_id = ?z)DELETE FROM messages WHERE session_id = ?z!DELETE FROM sessions WHERE id = ?T)rq   r  r~   rd   r„   rà   rc   )rt   Úexpected_idsÚremoved_idsrÎ   rÒ   s    €€€€r)   râ   z0SessionSessionsMixin.delete_session.<locals>._do  s¾   ø€ Ø�|‰|ÐIÈJÈ=ÓY×bÑbÓdÐlØØÐ'¨LØð=Ü8¸À
¸|ÓLð=ò -ð Ø×ÑÔ8¸À
¸|ÓLÔMØ�L‰LØZÐ]gÐ\iôð �L‰LÐDÀzÀmÔTØ�L‰LÐ<¸z¸mÔLØ×4Ñ4°TÔ:Ø×Ñ˜zÔ*Ør+   )rp   rå   r5  r  )	rÎ   rÒ   r*  r;  râ   Údeletedrv   r>  r?  s	   ``     @@r)   Údelete_sessionz#SessionSessionsMixin.delete_sessionù  s]   û€ ð "$ˆØ3FÐ3R”sÐ.Ô/ÐX\ˆ÷	ð  ×%Ñ% cÓ*ˆØò 	:ˆCØ×&Ñ& |°SÕ9ð	:ä�G‹}Ðr+   c                 ó`   ‡ ‡— ˆ ˆfd„}‰ j                  |«      }|r‰ j                  |‰«       |S )z�Delete *session_id* only if it has no messages, no title and no children; check and delete
        share one transaction so a concurrent flush can't be lost.c                 óˆ   •— | j                  d‰f«      }|j                  dkD  r‰j                  | «       |j                  dkD  S )Na¯  
                DELETE FROM sessions
                WHERE id = ?
                  AND title IS NULL
                  AND NOT EXISTS (
                      SELECT 1 FROM messages WHERE messages.session_id = sessions.id
                  )
                  AND NOT EXISTS (
                      SELECT 1 FROM sessions child
                      WHERE child.parent_session_id = sessions.id
                  )
                r   )rq   r	  rà   )rt   r}   rÎ   rÒ   s     €€r)   râ   z9SessionSessionsMixin.delete_session_if_empty.<locals>._do  sH   ø€ Ø—\‘\ðð �óˆFð �‰ Ò"Ø×8Ñ8¸Ô>Ø—?‘? QÑ&Ð&r+   ©rå   r5  )rÎ   rÒ   r*  râ   r@  s   ``   r)   Údelete_session_if_emptyz,SessionSessionsMixin.delete_session_if_empty  s2   ù€ õ	'ð& ×%Ñ% cÓ*ˆÙØ×&Ñ& |°ZÔ@Øˆr+   c           	      óä   ‡ ‡‡— t        |xs dD �ch c]  }t        |t        «      sŒ|sŒ|’Œ c}«      Š‰syg Šˆˆ ˆfd„}‰ j                  |«      }‰D ]  }‰ j	                  ||«       Œ |S c c}w )z¸Bulk delete with :meth:`delete_session` semantics per row, in ONE transaction. Unknown ids
        are skipped (UI selection can race another tab's delete). Returns the number deleted.r_   r   c           
      óþ  •— t        ‰«      D ��cg c]9  }| j                  dt        |«      › d�|«      j                  «       D ]  }|d   ‘Œ	 Œ; }}}|sy‰j	                  t        | |«      «       t        |«      D ]O  }t        |«      }| j                  d|› d�|«       | j                  d|› d�|«       | j                  d|› d�|«       ŒQ ‰j                  | «       ‰j	                  |«       t        |«      S c c}}w )Nz%SELECT id FROM sessions WHERE id IN (r]   ro   r   r�   r€   r‚   )r   rq   rb   rr   rd   r„   rà   r¬   )rt   r{   r!   Úexistingr|   r?  rÎ   Ú
unique_idss        €€€r)   râ   z1SessionSessionsMixin.delete_sessions.<locals>._do9  s  ø€ Ü/9¸*Ó/E÷  eÐQU×Q]ÑQ]Ø7Ô8QÐRWÓ8XÐ7YÐYZÐ[Ð]bóRç‰h‹jòÈ#˜˜D›	ð ˜	ð ˆHñ ñ ØØ×ÑÔ8¸¸xÓHÔIÜ# HÓ-ò P�Ü.¨uÓ5�Ø—‘Ø_Ð`bÐ_cÐcdÐeÐglôð —‘ÐIÈ"ÈÈQÐOÐQVÔWØ—‘ÐAÀ"ÀÀQÐGÈÕOðPð ×4Ñ4°TÔ:Ø×Ñ˜xÔ(Ü�x“=Ð ùós   �>C9)ra   r3   r4   rå   r5  )rÎ   r
  r*  rv   râ   r#  r?  rI  s   `     @@r)   Údelete_sessionsz$SessionSessionsMixin.delete_sessions2  sw   ú€ ô ¨+Ò*;¸Ö\ 3¼zÈ#ÌsÕ?SÒX[š3Ò\Ó]ˆ
ÙØØ!#ˆö	!ð" ×#Ñ# CÓ(ˆØò 	:ˆCØ×&Ñ& |°SÕ9ð	:àˆùò1 ]s   ‘A-§A-ªA-z‹message_count = 0 AND ended_at IS NOT NULL AND archived = 0 AND NOT EXISTS (SELECT 1 FROM messages WHERE messages.session_id = sessions.id)c                 óD   — | j                  d| j                  › �«      d   S )z^Count of empty, ended, non-archived sessions; ended_at guards a fresh session's first message.z$SELECT COUNT(*) FROM sessions WHERE r   )r=  Ú_EMPTY_SESSION_WHEREr…  s    r)   Úcount_empty_sessionsz)SessionSessionsMixin.count_empty_sessionsX  s&   € à�~‰~Ð DÀT×E^ÑE^ÐD_Ð`ÓaÐbcÑdÐdr+   c                 ón   ‡ ‡— g Šˆˆ fd„}‰ j                  |«      }‰D ]  }‰ j                  ||«       Œ |S )z�Delete every empty, ended, non-archived session in one transaction, orphaning (not cascading)
        children; transcript files are swept too.c                 ó   •— | j                  d‰j                  › �«      j                  «       D �ch c]  }|d   ’Œ	 }}|syt        |«      D ]`  }t	        |«      }| j                  d|› d�|«       | j                  d|› d�|«       | j                  d|› d�|«       ‰j                  |«       Œb ‰j                  | «       t        |«      S c c}w )Nrn   ro   r   r�   r]   r€   r‚   )rq   rL  rr   r   rb   rd   rà   r¬   )rt   r!   r
  r{   r|   r?  rÎ   s        €€r)   râ   z7SessionSessionsMixin.delete_empty_sessions.<locals>._do`  sä   ø€ Ø04·±Ø0°×1JÑ1JÐ0KÐLó1ç‰h‹jö¨˜3˜t›9ð ˆKð ñ ØÜ# KÓ0ò *�Ü.¨uÓ5�Ø—‘ÐhÐikÐhlÐlmÐnÐpuÔvð —‘ÐIÈ"ÈÈQÐOÐQVÔWØ—‘ÐAÀ"ÀÀQÐGÈÔOØ×"Ñ" 5Õ)ð*ð ×4Ñ4°TÔ:Ü�{Ó#Ð#ùòs   °CrD  )rÎ   r*  râ   r#  rv   r?  s   `    @r)   Údelete_empty_sessionsz*SessionSessionsMixin.delete_empty_sessions\  sE   ù€ ð "$ˆõ	$ð  ×#Ñ# CÓ(ˆØò 	:ˆCØ×&Ñ& |°SÕ9ð	:àˆr+   Úolder_than_daysc                 ó¨   — |j                  dd«       d|d<    | j                  d||dœ|¤Ž}|D ]  }| j                  |d   d«       Œ t        |«      S )aT  Bulk soft-hide with prune_sessions' filter surface, via set_session_archived so each lineage
        flips as a unit; idempotent. Returns matches. A lineage is matched through its TIP only: an
        old compression ancestor never qualifies on its own age, or the fan-out would hide an open,
        recently active continuation (#115489).rŠ  FTÚlineage_tips_only)rQ  rR   ro   r_   )Ú
setdefaultÚlist_prune_candidatesr�  r¬   )rÎ   rQ  rR   Úfiltersrø   r!   s         r)   Úarchive_sessionsz%SessionSessionsMixin.archive_sessionsu  sj   € ð 	×Ñ˜: uÔ-Ø'+ˆÐ#Ñ$Ø)ˆt×)Ñ)Ðd¸/ÐRXÑdÐ\cÑdˆØò 	7ˆCØ×%Ñ% c¨$¡i°Õ6ð	7ä�4‹yÐr+   Ú	idle_daysÚmin_interval_hoursÚexclude_pinnedc                 óü  — dddœ}	 t        j                   «       }	 t        | j                  d«      xs d«      }|r||z
  |dz  k  rd|d<   |S t        d	d
¬«       | j                  ||¬«      x}|d<   | j                  dt        |«      «       |dkD  rt        j                  d||«       |S # t        t        f$ r d}Y Œ…w xY w# t        $ r/}t        j                  d|«       t        |«      |d<   Y d}~|S d}~ww xY w)z­Idempotent, non-destructive auto-archive of sessions idle for ``idle_days``; state_meta
        ``last_auto_archive`` gates runs within ``min_interval_hours``. Never raises.Fr   )ÚskippedrŠ  Úlast_auto_archiver¡  i  Tr\  rü   Ústate_db_auto_archive)Úphase)rZ  rŠ  z=state.db auto-archive: archived %d session(s) idle >= %s daysz state.db auto-archive failed: %srŠ   N)rß   r7  Úget_metar8   r`  r   Úarchive_stale_sessionsÚset_metar4   rÌ   ÚinforË   Úwarning)	rÎ   rX  rY  rZ  r™  r  ÚlastrŠ  rë  s	            r)   Úmaybe_auto_archivez'SessionSessionsMixin.maybe_auto_archiveƒ  s  € ð
 .3ÀÑ!Bˆð	'Ü—)‘)“+ˆCðÜ˜TŸ]™]Ð+>Ó?ÒFÀ3ÓG�ñ ˜˜d™
Ð%7¸$Ñ%>Ò>Ø$(��yÑ!Ø�ô $ EÐ1HÕIØ,0×,GÑ,GÈ	ÐbpÐ,GÓ,qÐqˆH�v˜jÑ)à�M‰MÐ-¬s°3«xÔ8Ø˜!Š|Ü—‘ØSÐU]Ð_hôð ˆøô' œzÐ*ò Ø’ðûô  ò 	'Ü�N‰NÐ=¸sÔCÜ! #›hˆF�7�OØˆûð	'ús@   ‡C œB, ºC ÁAC Â,C Â=C Â?C Ã C Ã	C;Ã$C6Ã6C;)NNNNNNNNNNNNNNN)ÚunknownN)T)Úsession_reset)NNF)NNr°   )z1UPDATE sessions SET model_config = ? WHERE id = ?N)NNNNrË  r   Fr   TFFFNNFFNF)NrË  r   N)NNNr   FFFN)rð   )é   é   T)_Ú__name__Ú
__module__Ú__qualname__Ú__doc__r   r4   rÑ   Ústaticmethodr×   r   r   rç   rë   rí   r  rò   rû   Ú_ORPHAN_ADOPTION_MAX_GAP_Sr   Ú"_NON_CONTINUATION_CHILD_FILTER_SQLr  Útupler  r  r  r  r$  r,  r2  r7  r   r;  rB  rF  rI  r   rM  rV  r   rT  rY  rd  rh  rp  rt  rw  rf  r{  r€  rƒ  r†  r‰  r�  r   r�  r“  r›  r�  r¢  r¨  Ú	frozensetÚ_SESSION_COMPACT_EXCLUDEDrª  Ú__annotations__r
   r¶  rÊ  rì  Úclassmethodr>  ÚRowrð  r	  r  r  r  r  r   r	   r  r  r!  r&  r)  r   r5  r:  rA  rE  rJ  rL  rM  rP  rW  rf  r_   r+   r)   r¾   r¾   	  sf  … ÙYð 8¨C¡=ó ð" ð	A¸3ð 	AÀ4ò 	Aó ð	Að _cØUYØEIØVZØPTØ+/ñbOØðbOØ'*ðbOØ36ðbOØMQÐRUÐWZÐRZÉ^ðbOàðbOà,/ðbOàEMÈcÁ]ðbOð ðbOð ),ðbOð @CðbOð ð	bOð -0ð	bOð GOÈsÁmð	bOð
 ðbOð
 14ðbOð
 KNðbOð $ C™=ðbOð 
óbOðH¨ð °cð Èó ñ
¨ð °cð Ècð Ðgjó ñ
	
¨sð 	
¸tð 	
Ètó 	
ð JNØ!%ò"Øð"Ø),ð"Ø9AÀ#¹ð"à˜#‘ð"ð 
�#‰ó"ðD "'Ðð Ñ+Ð,AÐCUÓVÐWð XÙ+Ð,AÐCUÓVÐWð XÙ+Ð,AÀ?ÓSÐTð U8ð	8ð 'ð cð °sð ¸tó ð sð °Eð Àsð ÐTWð Ð\_ó ð!¨ð !°ó !ñ,°3ð Àð ÐZ^ó ð0 FJØFKñ(Øð(Ø$'ð(Ø5=¸c±]ð(à ‘}ð(à?Cð(ð 
�#‰ó(ðD W[Ø'+ñØðØ$'ðØ58ðØFNÈsÁmðà ‘}ðð 
óð*¨t°C¸°H©~ð À$ó ð 6:ð
Ø[_Ø37ò
Øð
Ø#+¨E¡?ð
ØKSÐTWÉ=ð
àÐ/Ñ0ð
ð 
ó
ð8
¸ð 
Àó 
ð8 OSñ
Øð
Ø25ð
Ø>FÀs¹mð
à	ó
ð!¨sð !À8ÈCÁ=ð !ÐUYó !ðb°Cð bÀXÈdÐSVÉiÑEXð bÐ]aó bñ
¨sð 
¸3ð 
È(ÐSVÉ-ð 
Ðcgó 
ð4 GØ=Añ!Øð!Ø&*¨3°¨8¡nð!àð!ð ˜ 8¨C¡= /°5Ð"8Ñ9Ñ:ð!ð 
ó	!ð" RXò6Ø #ð6Ø,0°°c°©Nð6ØKNó6ð&:°Sð :ÀÀcÈ3ÀhÁð :ÐTXó :ñR¸ð RÀ3ð RÐQTð RÐ`có Rð :>ÐY]Ø26ÐVZØò
Øð
Ø)1°#©ð
ØIQÐRUÉð
à  S¨# X¡Ñ/ð
àFNÈsÁmð
ð ð
ð 
ó	
ð,Q¨3ð Q¸ð QÀ$ó Qð ðd¨8°D¸¸c¸±NÑ+Cð dÈò dó ðdð< cð <¨h°t¸CÀ¸H±~Ñ.Fó <ð*¸ð *ÀÈ$ÈsÐTWÈxÉ.ÑAYó *ð,	?°sð 	?¸xÈ¹}ó 	?ð¸3ð À3ó ð&¨#ó ð&"¨#ð "¸3ð "Àsð "Ètó "ðHO¨sð O¸dð OÀtó Oð 7Ðð%¸ð %Àó %ðN¨Sð ¸$ð À4ó ð"K¨Sð K¸$ð KÀ4ó Kñb¨3ð b°dð bÀdó bð
 ð: D¨¨c¨¡Nð :°tò :ó ð:ñ !*ÚJó!Ðð 04Ð˜x¨™}Ó3àð ] sð  ]°sð  ]È3ð  ]ÐSXÐY\Ð^bÐcfÑ^gÐYgÑShò  ]ó ð ]ðD%°$°t¸CÀ¸H±~Ñ2Fð %ÐVZð %Ð_cÐdhÐilÐnqÐiqÑdrÑ_só %ðT Ø%)Ø!$Ø#Ø!òwð ðwð ˜c™ð	wð
 ðwð ðwð ðwð 
ˆd�3˜�8‰nÑ	ówðr ð˜GŸK™Kð ¨D°°c°©Nò ó ðð [_Ø[`ØEIØbgØMRØV[ñiØðiØ+/°©9ðiØNRÐSVÉiðiàðià'*ðià9<ðiàTXðið ðið ?Cðið #ð	ið ?Cð	ið \`ð	ið
 ðið
 -0ðið
 GKðið ðið 47ðið PTðið 
ˆd�3˜�8‰nÑ	óiðV°d¸3±ið ÀDÈÈcÈÁNó ñ4¨Sð ÀÈÁð ÐY\ó ð([°cð [¸dó [ð5°sð 5¸tÀC¹yó 5ð" ^_Ø!ñØ˜C ¨#¡°Ð4Ñ5ðØEHðØWZðàðð 
ˆd�3˜�8‰nÑ	óð4 PTØZ_ØEIñmØðmØ+/°©9ðmØILðmàðmà6:ðmàSWðmð ðmð :>¸c¹ðmð 
ó	mñP #ð P¨dó Pð
 +0ÀuØ!&òLØ#'ðLØ@DðLàðLð 
ˆc�3ˆh‰óLð&c°#ð c¸%ÀÀcÀ	Ñ:Jó cð ð¨H°T©Nð Èð ÐPTò ó ðð$
3°Sð 
3¸TÀ#¹Yó 
3ð ?CØ37ñØðØ-5°d©^ðà% d¨3¡iÑ0ðð 
óñ<°#ð ÀXÈdÁ^ð Ð_có ñ6¨4°©9ð ÀHÈTÁNð Ð^aó ðD	Jð ð
e có eñ°(¸4±.ð ÈCó ð4 FJñØ'¨™ðØ?Bðà	óð Z^ñØðØ8;ðØRVðà	ˆc�3ˆh‰ôr+   r¾   )r·   )r%   )Mrn  r.  r5   ÚloggingrÈ   r>  rß   Úpathlibr   Útypingr   r   r   r   r   r	   r
   r   Úagent.session_activityr   r   r   Úhermes_startup_watchdogr   Úhermes_state_commonr   r   r   r   r   r   r   r   r   r   r   r   r   r?   r   r   r   rb   Ú	getLoggerrÌ   r4   r*   r0   ÚobjectrZ  r:   rB   rH   rý  rO   r  r  rj   r~   r„   ÚINTERNAL_LISTING_SOURCESr˜   r—   r–   ÚSESSION_STATUS_EMPTYrs  r•   rš   Ú_SAME_KEY_NAMESPACE_SQLrN   rÝ   r®   Ú_INHERIT_SEPrÔ   Ú_FORK_EDGE_EXCLUSION_SQLrÕ   r¾   r_   r+   r)   ú<module>r…     s  ðñ:ó Û Û Û 	Û Û Ý ß N× NÓ N÷ñ õ <÷÷ ÷ ÷ ó ð 
ˆ×	Ñ	˜>Ó	*€ð^�t˜C ˜H‘~ð ^¨(°3©-ó ^ñ6˜Sð 6°có 6ñ #›HÐ ð6˜Sð 6 T¨#¨s¨(¡^ó 6ð 3ð ¨5°°d¸3±i°Ñ+@ó ð˜sð  u¨S°$°s±)¨^Ñ'<ó ð!Ø!4Ð 5ð 6 ð !6Ð6ð 7&ð	)Ð ñE˜˜S™	ð E¨ð E°có Eð !&°TÐPTØ¸$ÐRVØ°eÐV[ò#Øð#Ø/2ð#ØDHÈÁIð#àð#à.2°3©ið#àLOð#ð ð#ð 04ð#ð PTð#ð ˆ4�‰9�d˜3‘iÐÑ ó	#ðL6°$°s±)ð 6ÀÀSÁ	ó 6ð<°°S±	ð ¸dÀ3¹ió ð  9Ð à$Ð Ø*Ð ØÐ ØÐ ñ "Ò"LÓMÐ ð# (¨3¡-ð #Àð #ÐV^Ð_bÑVcð #Ðhkó #ð$[ð ð "ŸJ™Jñ 'ð[ô'ó Ð ñ˜#ð  cð °3ó ð  €à2Ø×Ñð Ù	NÐ'MÔ	Nðá˜¨¸UÐCZÐB[Ð[\Ð;]Ñ)]Ó^ñó ñð
 NñNð ð Ÿ7™7ñ #ð 7ô#ó Ð ð 3Ø×Ññ ð 6ô ó ñð
ñ
ð ñð ÷ Wò Wr+   