Ë
    âß²jSp  ã                  óî  — d Z ddlm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 ddlmZmZmZmZmZ ddlmZ  ej,                  d«      Z ed	«      Zd
ZdZd>d„Zd?d„Zd@dAd„ZdBd„Zddd„ fdd eedz   «      fdd ed«      fdd ed«      fdd ed«      fdd ede«      fdd ed«      fd defd!d ed"«      fd#d ed$«      fd%d ed&e«      fd'd ed(ej@                  «      fd)d ed*«      fd+d ed,«      fd-d ed.«      fd/d ed0e«      fd1d eedz   «      fd2d eed3z   «      fd4d eedz   «      fd5d eed3z   «      fd6d ed7«      fd8d ed9«      ffZ! e"d:„ e!D «       «      h d;£z  Z# G d<„ d=«      Z$y)CzQRetention pruning, stale-session archiving and VACUUM policy mixin for SessionDB.é    )ÚannotationsN)ÚPath)ÚAnyÚDictÚListÚOptionalÚTuple)ÚAUTO_VACUUM_MIN_FREELIST_RATIOÚ
_id_chunksÚ_placeholdersÚ_sql_session_last_activeÚescape_like)Úreport_startup_progressÚhermes_stateÚsz<(COALESCE(s.input_tokens, 0) + COALESCE(s.output_tokens, 0))z4COALESCE(s.actual_cost_usd, s.estimated_cost_usd, 0)c                ó<   — dt        | j                  «       «      › d�S )Nú%)Ú_escape_likeÚlower)Úvalues    ú=/home/ubuntu/.hermes/hermes-agent/hermes_state_maintenance.pyÚ_liker      s   € ØŒ|˜EŸK™K›MÓ*Ð+¨1Ð-Ð-ó    c                ó@   — ddl m}  || «      \  }}|gt        |«      fS )Nr   )Ú_cwd_prefix_clause)Úhermes_state_sessionsr   Úlist)r   r   ÚclauseÚparamss       r   Ú_cwd_prefix_filterr       s$   € Ý8Ù'¨Ó.�N€FˆFØˆ8”T˜&“\Ð!Ð!r   c                ó   ‡ ‡— ˆ ˆfd„S )Nc                ó(   •— ‰g‰r
 ‰| «      gfS | gfS ©N© )Úvr   Úconvs    €€r   ú<lambda>z_one.<locals>.<lambda>"   s!   ø€ �v�h©D¡ a£Ð 8Ð9€ °aÐ 8Ð9€ r   r$   )r   r&   s   ``r   Ú_oner(   !   s	   ù€ Ü9Ð9r   c                óR   — 	 |r| t        |«      z
  S dS # t        t        f$ r Y yw xY w)zKAge of a state_meta timestamp; None when unset or corrupt (= no prior run).N)ÚfloatÚ	TypeErrorÚ
ValueError)ÚnowÚraws     r   Ú_seconds_sincer/   %   s4   € ðÙ#&ˆs”U˜3“ZÑÐ0¨DÐ0øÜ”zÐ"ò Ùðús   ‚ ’ ”&¥&Úlast_active_beforeÚnotnonec                ó    — t         dz   dg| | gfS )Nú < ?zG(COALESCE(s.end_reason, '') != 'startup_orphan_reap' OR s.ended_at < ?))Ú_LAST_ACTIVE_SQL)r%   s    r   r'   r'   1   s!   € Ü	˜FÑ	"Ø	Rð	Tà	
ˆAˆð1€ r   Úlast_active_afterz >= ?Ústarted_beforezs.started_at < ?Ústarted_afterzs.started_at >= ?ÚsourceÚtruthyzs.source = ?Ú
title_likez.LOWER(COALESCE(s.title, '')) LIKE ? ESCAPE '\'Ú
end_reasonzs.end_reason = ?Ú
cwd_prefixÚmin_messageszs.message_count >= ?Úmax_messageszs.message_count <= ?Ú
model_likez.LOWER(COALESCE(s.model, '')) LIKE ? ESCAPE '\'Úproviderz+LOWER(COALESCE(s.billing_provider, '')) = ?Úuser_idzs.user_id = ?Úchat_idzs.chat_id = ?Ú	chat_typezs.chat_type = ?Úbranch_likez3LOWER(COALESCE(s.git_branch, '')) LIKE ? ESCAPE '\'Ú
min_tokensÚ
max_tokensz <= ?Úmin_costÚmax_costÚmin_tool_callsz#COALESCE(s.tool_call_count, 0) >= ?Úmax_tool_callsz#COALESCE(s.tool_call_count, 0) <= ?c              #  ó(   K  — | ]
  \  }}}|–— Œ y ­wr#   r$   )Ú.0ÚnameÚ_s      r   ú	<genexpr>rO   K   s   è ø€ ÒF©¨¨q°!¤ÑFùs   ‚>   ÚarchivedÚinclude_pinnedÚlineage_tips_onlyc                  ó$  — e Zd ZdZddd„Zdd„Zddddddd	œ	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d d
„Zeddddœ	 	 	 d!d„«       Zd"d„Z	d#	 d$d„Z
d#	 d%d„Zd#	 d%d„Zddœd&d„Z	 	 d'	 	 	 	 	 d(d„Zd)d„Zd*d„Zd+d„Zd,d„Zd-d„Zdddddef	 	 	 	 	 	 	 	 	 	 	 	 	 d.d„Zy)/ÚSessionMaintenanceMixinzKRetention pruning, stale-session archiving and VACUUM policy for SessionDB.Nc                óº   ‡ ‡— t        j                   «       dz
  Šˆˆ fd„}‰ j                  |«      xs g }|r|ndD ]  }‰ j                  ||«       Œ t        |«      S )zCRemove empty TUI ghost sessions (no messages, no title, >24hr old).é€Q c                óø   •— | j                  d‰f«      j                  «       D �cg c]  }|d   ‘Œ	 }}t        |«      D ]!  }| j                  dt        |«      › d�|«       Œ# |r‰j	                  | «       |S c c}w )NaZ  
                SELECT id FROM sessions
                WHERE source = 'tui'
                  AND title IS NULL
                  AND ended_at IS NOT NULL
                  AND started_at < ?
                  AND NOT EXISTS (
                      SELECT 1 FROM messages WHERE messages.session_id = sessions.id
                  )
            r   ú"DELETE FROM sessions WHERE id IN (ú))ÚexecuteÚfetchallr   r   Ú#_delete_unreferenced_system_prompts)ÚconnÚrÚidsÚchunkÚcutoffÚselfs       €€r   Ú_doz?SessionMaintenanceMixin.prune_empty_ghost_sessions.<locals>._doT   s‘   ø€ Ø!%§¡ð 	/ð �ó	"÷ %™H›Jö	(˜A�1�Q“4ð 	(ˆCð 	(ô $ C›ò b�Ø—‘ÐAÄ-ÐPUÓBVÐAWÐWXÐYÐ[`ÕaðbáØ×8Ñ8¸Ô>ØˆJùò	(s   ¥A7r$   )ÚtimeÚ_execute_writeÚ_remove_session_filesÚlen)rb   Úsessions_dirrc   Úremoved_idsÚsidra   s   `    @r   Úprune_empty_ghost_sessionsz2SessionMaintenanceMixin.prune_empty_ghost_sessionsQ   s]   ù€ ä—‘“˜uÑ$ˆõ	ð  ×)Ñ)¨#Ó.Ò4°"ˆÙ".‘;°Bò 	:ˆCØ×&Ñ& |°SÕ9ð	:ä�;ÓÐr   c                ój   — ddl m} ddlm} 	  | j                  ||fdddddœ|¤Ž y# ||f$ r Y yw xY w)z‘True when a live turn lease / compression lock protects ``sid``; expired or
        dead-holder guards are reclaimed and fenced as a side effect.r   )Ú!SessionCompressionInProgressError)ÚSessionTurnLeaseLostErrorNT)Úcompression_lock_holderÚturn_lease_holderÚreject_active_turn_leaseÚreject_active_compression_lockF)r   rm   Úhermes_state_errorsrn   Ú_check_transcript_write_guards)rb   r]   rj   Úkwargsrm   rn   s         r   Ú_write_guards_rejectz,SessionMaintenanceMixin._write_guards_rejecti   s^   € õ 	CÝAð	Ø/ˆD×/Ñ/Ø�cð^Ø37È4Ø)-Èdñ^àV\ò^ð
 øð 2Ð3LÐMò 	Ùð	ús   Ž( ¨2±2)ÚtuiÚdesktopÚsubagentr$   FT)ÚsourcesÚexclude_idsÚexclude_pinnedÚheartbeat_staleness_secondsÚ!heartbeat_ownership_grace_secondsÚrespect_gateway_heartbeatsc               óf  ‡ ‡‡‡— t        d„ |D «       «      }|dk  s|sg S ||}
}	|	r|	dkD  s|dz  }	|
�|
dk\  s|	}
t        j                  «       x}|z
  }|rdnd}dt        d«      › d�}d	}|r|d
z  }||	z
  |
f}dt        |«      › d|› d|› �Šg |¢|‘|‘|¢­Šˆˆˆˆ fd„}‰ j	                  |«      xs g S )u  Close session rows orphaned by a dead gateway process (its in-process disconnect grace timer died
        with it, leaving ``ended_at IS NULL`` forever).  Rows of ``sources`` whose ``started_at`` AND
        canonical last activity are both older than ``max_idle_seconds`` get
        ``end_reason='startup_orphan_reap'`` (the ``started_at`` predicate protects fresh compression/branch
        children whose copied activity is old).  Only pass sources whose lifecycle the caller owns â€” never
        messaging platforms like ``telegram`` (ending those triggers a routing loop).  ``exclude_ids`` spares
        rows this process still holds.  Non-destructive: messages kept, row resumable, first-reason-wins.
        With ``respect_gateway_heartbeats`` a row is reaped only when no live backend (heartbeat within
        ``heartbeat_staleness_seconds``, default ``2 * max_idle_seconds``) could own it: B owns S if
        ``B.started_at <= S.started_at + grace`` (default = staleness) â€” grace covers a migrating backend
        whose sessions predate its first heartbeat, bounded so a PID-reuse respawn cannot protect rows
        forever.  Disable the gate only for state.db-owned sources.  SELECT, live-lease validation and UPDATE
        run in one ``BEGIN IMMEDIATE`` transaction; active leases/locks spare the row, expired guards are
        removed so their owner is fenced.

        See #65194.
        ``exclude_pinned`` is intended for broad automatic sweeps; pinned rows remain explicitly
        recoverable. See #60609.
        c              3  ó&   K  — | ]	  }|sŒ|–— Œ y ­wr#   r$   )rL   r   s     r   rO   zBSessionMaintenanceMixin.sweep_orphaned_sessions.<locals>.<genexpr>�   s   è ø€ Ò-˜1ª1”QÑ-ùs   ‚Šr   é   z AND COALESCE(pinned, 0) = 0Ú zstarted_at < ? AND Úsessionsr3   r$   z| AND NOT EXISTS (SELECT 1 FROM gateway_heartbeats h WHERE h.last_heartbeat >= ? AND h.started_at <= sessions.started_at + ?)z AND source IN (rY   ú AND c                ój  •— | j                  d‰› �‰«      j                  «       }‰D �ch c]  }|sŒt        |«      ’Œ }}d„ |D «       D �cg c]  }||vr‰	j                  | |«      s|‘Œ }}|sg S | j                  dt	        |«      › d‰› �t        j
                  «       g|¢‰¢­«       |S c c}w c c}w )Nz.SELECT id FROM sessions WHERE ended_at IS NULLc              3  ó8   K  — | ]  }t        |d    «      –— Œ y­w)ÚidN)Ústr)rL   Úrows     r   rO   zOSessionMaintenanceMixin.sweep_orphaned_sessions.<locals>._do.<locals>.<genexpr>¥   s   è ø€ Ò&F¸#¤s¨3¨t©9§~Ñ&Fùs   ‚zRUPDATE sessions SET ended_at = ?, end_reason = 'startup_orphan_reap' WHERE id IN (z) AND ended_at IS NULL)rZ   r[   r‰   rv   r   rd   )
r]   ÚrowsÚxÚexcludedrj   Úvictimsr{   Úscope_paramsÚ	scope_sqlrb   s
         €€€€r   rc   z<SessionMaintenanceMixin.sweep_orphaned_sessions.<locals>._do¡   sÙ   ø€ Ø—<‘<Ð"PÐQZÐP[Ð \Ø ,ó.ß.6©h«jð à(3Ö9 1²qœ˜A�Ð9ˆHÐ9Ù&FÀÔ&Fö \˜sØ XÑ-°d×6OÑ6OÐPTÐVYÔ6Zò ð \ˆGð \áØ�	à�L‰Lð!Ü!.¨wÓ!7Ð 8Ð8NÈyÈkð[ä—‘“Ð6˜wÐ6¨Ñ6ô8ð ˆNùò :ùò\s   ©B+±B+ÁB0)Útuplerd   r   r   re   )rb   Úmax_idle_secondsrz   r{   r|   r}   r~   r   ÚsrcsÚhb_stalenessÚhb_gracer-   ra   Ú	pin_scopeÚorphan_predicateÚheartbeat_paramsrc   r�   r�   s   `  `             @@r   Úsweep_orphaned_sessionsz/SessionMaintenanceMixin.sweep_orphaned_sessionsv   s  û€ ô2 Ñ- Ô-Ó-ˆØ˜qÒ ©ØˆIØ!<Ð>_�hˆÙ °Ò!1Ø+¨aÑ/ˆLØÐ$¨°QªØ#ˆHÜŸ™›Ð$�#Ð(8Ñ8ˆÙ6DÑ2È"ˆ	Ø0Ô1IÈ*Ó1UÐ0VÐVZÐ[ÐØ.0ÐÙ%Øð "gñ hÐà # lÑ 2°HÐ=ÐØ&¤}°TÓ':Ð&;¸1¸Y¸KÀuÐM]ÐL^Ð_ˆ	ØA˜ÐA˜vÐA vÐAÐ0@ÑAˆ÷	ð ×"Ñ" 3Ó'Ò-¨2Ð-r   )rP   rQ   rR   c                óø  — t        |«      t        z
  }|rt        dt        |«      d   ›�«      ‚dg}|r|j	                  d«       g }t
        D ]Y  \  }}}	|j                  |«      }
|dk(  r|
€Œ nt        |
«      sŒ- |	|
«      \  }}|j                  |«       |j                  |«       Œ[ t        | t        «      r|j	                  dt        | «      › �«       |s|j	                  d«       dj                  |«      |fS )	u  Shared WHERE clause for bulk prune/archive selection (alias ``s``): ``_PRUNE_FILTERS``
        AND together, only ended sessions are ever candidates, ``archived`` is tri-state
        (None = both), ``*_like`` are case-insensitive substrings, the rest exact.
        ``lineage_tips_only`` (bulk archive) drops compression ancestors: they are archived with
        their tip, never on their own age â€” matching an old ancestor would fan out over the lineage
        and hide its OPEN, recently active tip (#115489).zQSessionMaintenanceMixin._prune_filter_where() got an unexpected keyword argument r   ús.ended_at IS NOT NULLz+COALESCE(s.end_reason, '') <> 'compression'r1   zs.archived = zCOALESCE(s.pinned, 0) = 0r…   )ÚsetÚ_PRUNE_FILTER_NAMESr+   ÚsortedÚappendÚ_PRUNE_FILTERSÚgetÚboolÚextendÚ
isinstanceÚintÚjoin)rP   rQ   rR   ÚfiltersÚunknownÚclausesr   rM   ÚappliesÚbuildr   Únew_clausesÚ
new_paramss                r   Ú_prune_filter_wherez+SessionMaintenanceMixin._prune_filter_where±   s   € ô �g“,Ô!4Ñ4ˆÙÜð 0Ü06°w³ÀÑ0BÐ/EðGó Hð Hà+Ð,ˆÙØ�N‰NÐHÔIØˆÜ$2ò 	*Ñ ˆD�'˜5Ø—K‘K Ó%ˆEØ&-°Ò&:�Ò!ÄÀUÅÙ*/°«,Ñ'�˜ZØ—‘˜{Ô+Ø—‘˜jÕ)ð	*ô �h¤Ô%Ø�N‰N˜]¬3¨x«=¨/Ð:Ô;áØ�N‰NÐ6Ô7Ø�|‰|˜GÓ$ fÐ,Ð,r   c                óÔ   — |�S|j                  d«      €B|j                  d«      €1|dk  rt        d|›d�«      ‚t        j                  «       |dz  z
  |d<    | j                  dd|i|¤ŽS )	zRTranslate the legacy age window into the shared activity filter, then build WHERE.r0   r6   r   z"older_than_days must be >= 0, got zO: a negative retention builds a future cutoff that matches every ended session.rV   r8   r$   )r¡   r,   rd   r®   )rb   Úolder_than_daysr8   r§   s       r   Ú_prune_wherez$SessionMaintenanceMixin._prune_whereÏ   s‹   € àÐ'¨G¯K©KÐ8LÓ,MÐ,UØ—K‘KÐ 0Ó1Ð9Ø Ò"Ü Ø8¸Ð8Kð LYð YóZð Zô -1¯I©I«K¸?ÈUÑ;RÑ,SˆGÐ(Ñ)Ø'ˆt×'Ñ'ÑA¨vÐA¸ÑAÐAr   c                ó    — | j                  |||«      \  }}| j                  dt        › d|› d�|«      D �cg c]  }t        |«      ‘Œ c}S c c}w )z¾Dry-run: sessions a matching prune/archive would touch, oldest first (``older_than_days``
        = inactivity threshold: freshest of ``last_activity_at`` / latest message / ``started_at``).zRSELECT s.id, s.source, s.title, s.model, s.started_at,
                           z~ AS last_active,
                           s.ended_at, s.message_count, s.archived
                    FROM sessions s WHERE z?
                    ORDER BY last_active ASC, s.started_at ASC)r±   Ú	_read_allr4   Údict)rb   r°   r8   r§   Úwherer   rŠ   s          r   Úlist_prune_candidatesz-SessionMaintenanceMixin.list_prune_candidatesÚ   sq   € ð ×)Ñ)¨/¸6À7ÓK‰ˆˆvØ%)§^¡^ðÜ,Ð-ð .+à+0¨'ð 2?ðBð DJó&Kö L˜c”�S•	ò Lð 	Lùò Ls   ¶Ac                óp   — | j                  |||«      \  }}t        | j                  d|› �|«      d   «      S )zNCount-only :meth:`list_prune_candidates` (CLI reports spared pinned sessions).ú&SELECT COUNT(*) FROM sessions s WHERE r   )r±   r¥   Ú	_read_one)rb   r°   r8   r§   rµ   r   s         r   Úcount_prune_matchesz+SessionMaintenanceMixin.count_prune_matchesæ   sA   € ð ×)Ñ)¨/¸6À7ÓK‰ˆˆvÜ�4—>‘>Ð$JÈ5È'Ð"RÐTZÓ[Ð\]Ñ^Ó_Ð_r   c                óÎ   — | j                  |||«      \  }}d}|j                  |«      st        d«      ‚d|t        |«      d › �}t	        | j                  d|› �|«      d   «      S )zZCount open sessions a matching prune skips (``ended_at`` guard inverted); visibility-only.r›   z0prune filter lost its ended-session safety guardzs.ended_at IS NULLNr¸   r   )r±   Ú
startswithÚRuntimeErrorrg   r¥   r¹   )rb   r°   r8   r§   rµ   r   Úended_guardÚ
open_wheres           r   Úcount_open_prune_matchesz0SessionMaintenanceMixin.count_open_prune_matchesì   sz   € ð ×)Ñ)¨/¸6À7ÓK‰ˆˆvØ.ˆØ×Ñ Ô,ÜÐQÓRÐRØ)¨%´°KÓ0@Ð0AÐ*BÐ)CÐDˆ
Ü�4—>‘>Ð$JÈ:È,Ð"WÐY_Ó`ÐabÑcÓdÐdr   )r|   c               ó  — |�|dk  ryt        j                   «       t        |«      dz  z
  }|rdnd}| j                  d|› dt        › d�| j                  |f«      }|D ]  }| j                  |d   d«       Œ t        |«      S )	u…  Archive every session untouched for ``idle_days`` (freshest of ``last_activity_at`` /
        latest message / ``started_at``); may archive unended sessions.  ``archived = 0`` makes
        repeats no-ops; only lineage tips (``end_reason <> 'compression'``) are candidates â€” a
        stale tip archives its chain via :meth:`set_session_archived`, so an old compressed-away
        root with a recent continuation is never matched.  The hidden canonical Bot Chat (same
        predicate as :meth:`set_session_pinned`) is exempt: only a deliberate archive may retire
        it, since archiving releases its registry title to the next Bot open.r   ç     õ@zAND s.pinned = 0rƒ   z–
            SELECT s.id FROM sessions s
            WHERE s.archived = 0
              AND COALESCE(s.end_reason, '') <> 'compression'
              zd
              AND NOT (COALESCE(s.hidden, 0) <> 0 AND COALESCE(s.title, '') = ?)
              AND z7 < ?
            ORDER BY s.started_at ASC
            T)rd   r*   r³   r4   ÚCANONICAL_BOT_CHAT_TITLEÚset_session_archivedrg   )rb   Ú	idle_daysr|   ra   Ú
pin_clauser‹   rŠ   s          r   Úarchive_stale_sessionsz.SessionMaintenanceMixin.archive_stale_sessionsö   s¥   € ð Ð 	¨A¢ØÜ—‘“œu YÓ/°'Ñ9Ñ9ˆÙ+9Ñ'¸rˆ
Ø�~‰~ðð ˆlð ä#Ð$ð %ðð ×/Ñ/°Ð8ó	:ˆð ò 	4ˆCØ×%Ñ% c¨!¡f¨dÕ3ð	4ä�4‹yÐr   éZ   c                ó¦   ‡ ‡‡	‡
‡— ‰ j                  |||«      \  Š
Šg Š	ˆˆ	ˆ ˆ
ˆfd„}‰ j                  |«      }‰	D ]  }‰ j                  ||«       Œ |S )a  Delete ended sessions inactive for ``older_than_days`` (an explicit ``started_before`` /
        ``last_active_before`` overrides it; None = no implicit bound) matching the filters.
        Children outside the window are orphaned (parent NULLed), not cascade-deleted.  With
        *sessions_dir*, transcript files are removed outside the DB transaction.
        ``exclude_active_write_guards`` (automatic maintenance) skips rows under a live turn lease
        or compression lock while expired/dead holders are reclaimed and fenced.c           	     óî  •— | j                  d‰
› �‰«      }|j                  «       D �ch c]  }|d   ’Œ	 }}‰r'||D �ch c]  }‰	j                  | |d¬«      r|’Œ c}z  }|syt        |«      D ]`  }t	        |«      }| j                  d|› d�|«       | j                  d|› d�|«       | j                  d	|› d�|«       ‰j                  |«       Œb ‰	j                  | «       t        |«      S c c}w c c}w )
Nz"SELECT s.id FROM sessions s WHERE rˆ   T)Úallow_closed_compression_parentr   zIUPDATE sessions SET parent_session_id = NULL WHERE parent_session_id IN (rY   z*DELETE FROM messages WHERE session_id IN (rX   )rZ   r[   rv   r   r   r£   r\   rg   )r]   ÚcursorrŠ   Úsession_idsrj   r`   ÚphÚexclude_active_write_guardsri   rb   rµ   Úwhere_paramss          €€€€€r   rc   z3SessionMaintenanceMixin.prune_sessions.<locals>._do  s  ø€ Ø—\‘\Ð$FÀuÀgÐ"NÐP\Ó]ˆFØ06·±Ó0AÖB¨˜3˜t›9ÐBˆKÐBÙ*Ø¨{ö  o¨Ø#'×#<Ñ#<¸TÀ3ÐhlÐ#<Ô#mò !$ò  oñ o�áØä# KÓ0ò *�Ü" 5Ó)�Ø—‘ÐhÐikÐhlÐlmÐnÐpuÔvØ—‘ÐIÈ"ÈÈQÐOÐQVÔWØ—‘ÐAÀ"ÀÀQÐGÈÔOØ×"Ñ" 5Õ)ð*ð ×4Ñ4°TÔ:Ü�{Ó#Ð#ùò Cùò os
   ©C-¾C2)r±   re   rf   )rb   r°   r8   rh   rÏ   r§   rc   Úcountrj   ri   rµ   rÐ   s   `   `    @@@r   Úprune_sessionsz&SessionMaintenanceMixin.prune_sessions  sc   ü€ ð #×/Ñ/°ÀÈÓQÑˆˆ|Ø!#ˆ÷	$ð 	$ð" ×#Ñ# CÓ(ˆØò 	:ˆCØ×&Ñ& |°SÕ9ð	:àˆr   c           
     óV  — 	 | j                  «       5 }| j                  €
	 ddd«       y|D �cg c]0  }t        |j                  d|› �«      j	                  «       d   «      ‘Œ2 c}cddd«       S c c}w # 1 sw Y   yxY w# t
        $ r }t        j                  ||«       Y d}~yd}~ww xY w)z_Integer PRAGMAs over the existing connection (never a byte probe); None + debug log on failure.NzPRAGMA r   )Ú	_read_ctxÚ_connr¥   rZ   ÚfetchoneÚ	ExceptionÚloggerÚdebug)rb   ÚnamesÚfail_msgr]   rM   Úexcs         r   Ú_page_pragmasz%SessionMaintenanceMixin._page_pragmas1  s¨   € ð	Ø—‘Ó!ð ] TØ—:‘:Ð%Ø÷]ð ]ð W\Ö\Èdœ˜DŸL™L¨7°4°&Ð)9Ó:×CÑCÓEÀaÑHÕIÒ\÷]ñ ]ùò ]÷]ð ]ûô ò 	Ü�L‰L˜ 3Ô'Üûð	úsO   ‚A? ’A3 A? ©A3­5A.Á"A3Á$	A? Á.A3Á3A<Á8A? Á<A? Á?	B(ÂB#Â#B(c                óD   — | j                  dd«      }|€dS |d   |d   z  S )aL  ``page_count * page_size``: main-file size once the WAL is checkpointed in.  Prefer
        over ``os.path.getsize`` when reporting a VACUUM: in WAL mode the rewrite lands in
        ``-wal`` and the checkpoint is refused while another connection holds a read-mark, so
        a stat() delta understates the win and can go negative.)Ú
page_countÚ	page_sizez"Could not read logical DB size: %sNr   é   ©rÝ   ©rb   Úvaluess     r   Úlogical_size_bytesz*SessionMaintenanceMixin.logical_size_bytes<  s4   € ð
 ×#Ñ#Ð$?ÐAeÓfˆØ�~ˆtÐ@¨6°!©9°v¸a±yÑ+@Ð@r   c                óX   — | j                  dd«      }|€dS |d   dkD  r|d   |d   z  S dS )uù  Reclaimable fraction (``freelist_count / page_count``) gating VACUUM in
        :meth:`maybe_auto_prune_and_vacuum`; None = fall back to the time throttle.

        ``PRAGMA freelist_count / PRAGMA page_count`` read over the existing connection (never a byte-level
        probe of the live file â€” see ``sqlite_safe_read``). This is what VACUUM would actually give back; it
        is the gate :meth:`maybe_auto_prune_and_vacuum` uses to decide whether a full rewrite pays off
        (#54189).
        )rß   Úfreelist_countz!Could not read freelist ratio: %sNr   rá   g        râ   rã   s     r   Ú_freelist_ratioz'SessionMaintenanceMixin._freelist_ratioD  sE   € ð ×#Ñ#Ð$DÐFiÓjˆØ�~ˆtÐ\ÀVÈAÁYÐQRÂ]¨F°1©I¸¸q¹	Ñ,AÐ\ÐX[Ð\r   c                óœ   — 	 | j                   j                  d|› d�«       y # t        $ r }t        j	                  ||«       Y d }~y d }~ww xY w)NzPRAGMA wal_checkpoint(rY   )rÕ   rZ   r×   rØ   rÙ   )rb   ÚmoderÛ   rÜ   s       r   Ú_try_checkpointz'SessionMaintenanceMixin._try_checkpointP  sD   € ð	(Ø�J‰J×ÑÐ!7¸°v¸QÐ?Õ@øÜò 	(Ü�L‰L˜ 3×'Ñ'ûð	(ús   ‚" ¢	A«AÁAc                óâ  — | j                  «        d}	 | j                  «       }| j
                  5  | j                  «        | j                  €| j                  d¬«       | j                  dd«       | j                  j                  d«       | j                  d	d
«       | j                  «        ddd«       |S # t        $ r }t        j	                  d|«       Y d}~Œ¸d}~ww xY w# 1 sw Y   |S xY w)uz  VACUUM to reclaim space after large deletes (SQLite never shrinks on its own).
        Takes an exclusive lock â€” callers must ensure no other writers are active.  FTS5
        segments are merged first (:meth:`optimize_fts`) so their pages are reclaimed too;
        returns the number of FTS indexes optimized (0 on merge failure / no FTS). A quarantined
        handle (corrupt image, replaced file, lost WAL generation) raises before any rewrite: a
        VACUUM reads every page and commits the result back, turning contained damage into an
        amplified one (#105670). Same guard ``_execute_write`` applies to every write.r   z%FTS optimize before VACUUM failed: %sNÚwrite)ÚcontextÚPASSIVEz1WAL checkpoint (PASSIVE) before VACUUM failed: %sÚVACUUMÚTRUNCATEz1WAL checkpoint (TRUNCATE) after VACUUM failed: %s)Ú_raise_if_db_corruptÚoptimize_ftsr×   rØ   ÚwarningÚ_lockÚ_raise_if_db_replacedrÕ   Ú_reopen_after_close_lockedrë   rZ   Ú_record_db_file_identity)rb   Ú	optimizedrÜ   s      r   ÚvacuumzSessionMaintenanceMixin.vacuumV  sã   € ð 	×!Ñ!Ô#Øˆ	ð	IØ×)Ñ)Ó+ˆIð �Z‰Zñ 	,Ø×&Ñ&Ô(Ø�z‰zÐ!Ø×/Ñ/¸Ð/Ô@ð × Ñ  Ð,_Ô`Ø�J‰J×Ñ˜xÔ(à× Ñ  Ð-`Ôað ×)Ñ)Ô+÷	,ð Ðøô ò 	IÜ�N‰NÐBÀC×HÑHûð	Iú÷	,ð Ðús#   ”B8 °A>C$Â8	C!ÃCÃC!Ã$C.é   é   c           
     óÞ  — ddl m}m} dddddœ}	|�|dk  rt        j	                  d|«       d|	d<   |	S  || j
                  «      }
|
€d|	d<   |	S 	 t        j                  «       }t        || j                  d	«      «      }|�||d
z  k  rd|	d<   |	 ||
«       S t        dd¬«       | j                  ||d¬«      x|	d<   }t        dd¬«       | j                  t        |«      dz  | j                  dd¬«      }t        |«      |	d<   t        || j                  d«      «      }|du xs ||dz  k\  }|rø|dkD  ró|rñ| j                  «       x|	d<   }ddlm}m}  || j
                  «       || j
                  | ¬«      z   }|rHt        |«      |	d<   t        j'                  dt        |«      dj)                  d„ |dd D «       «      «       nc|�||kD  r?	 t        dd¬«       | j+                  «        d|	d<   | j-                  dt/        |«      «       nt        j'                  d!|d"z  |d"z  «       | j-                  d	t/        |«      «       |s|dkD  r)t        j3                  d#t        |«      |||	d   rd$nd%«        ||
«       |	S # t0        $ r }t        j	                  d |«       Y d}~Œyd}~ww xY w# t0        $ r.}t        j	                  d&|«       t/        |«      |	d'<   Y d}~Œhd}~ww xY w#  ||
«       w xY w)(a¥  Idempotent startup auto-maintenance (never raises): prune inactive sessions, reap stale open
        state-owned rows, optional VACUUM.  Runs at most once per ``min_interval_hours``; VACUUM has its own
        ``min_vacuum_interval_days`` throttle and also requires ``freelist_count / page_count`` >
        ``min_vacuum_freelist_ratio`` so a small prune on a dense multi-GB database never triggers a full
        rewrite.  Stale-open reconciliation: cron/kanban/subagent/one-shot CLI rows never set ``ended_at``
        when their process dies and prune only deletes ended rows, so after pruning, open rows from
        :attr:`_AUTO_PRUNE_STALE_OPEN_SOURCES` older than ``retention_days`` are closed
        (``startup_orphan_reap``); they stay resumable and age from their close.  Returns ``{"skipped",
        "pruned", "closed", "vacuumed"}`` plus ``"freelist_ratio"`` when a VACUUM was considered and
        ``"error"`` on failure.

        Records the last run timestamp in state_meta so subsequent calls within ``min_interval_hours``
        no-op. Designed to be called once at startup from long-lived entrypoints (CLI, gateway, cron
        scheduler). See #54189.
        When *sessions_dir* is provided, on-disk transcript files (``.json`` / ``.jsonl`` /
        ``request_dump_*``) for pruned sessions are removed as part of the same sweep (issue #3015).
        Messaging and UI sources are never touched here. See #54189.
        r   )Ú_release_auto_maintenance_lockÚ"_try_acquire_auto_maintenance_lockF)ÚskippedÚprunedÚclosedÚvacuumedNz«state.db auto-maintenance skipped: sessions.retention_days=%r is outside the allowed range (a whole number of days >= 0); set sessions.auto_prune: false to disable pruningTr   Úlast_auto_prunei  g      Œ@Ústate_db_auto_prune)Úphase)r°   rh   rÏ   r  Ústate_db_auto_sweeprÂ   )r’   rz   r|   r   r  Úlast_vacuumrV   Úfreelist_ratio)Úforeign_state_db_holdersÚin_process_state_db_holders)ÚexcludeÚvacuum_skipped_holderszestate.db auto-maintenance: skipping VACUUM, %d other process(es) hold the store or a WAL sidecar (%s)z, c              3  ó0   K  — | ]  \  }}|› d |› �–— Œ y­w)ú:Nr$   )rL   ÚpidÚtargets      r   rO   zFSessionMaintenanceMixin.maybe_auto_prune_and_vacuum.<locals>.<genexpr>Ä  s   è ø€ Ò/aÁkÀcÈ6°3°%°q¸¸Ô0AÑ/aùs   ‚é   Ústate_db_auto_vacuumr  zstate.db VACUUM failed: %sz_state.db auto-maintenance: skipping VACUUM, only %.1f%% of pages reclaimable (threshold %.0f%%)g      Y@zgstate.db auto-maintenance: closed %d stale open session(s), pruned %d session(s) inactive for %d days%sz	 + VACUUMrƒ   z$state.db auto-maintenance failed: %sÚerror)Úhermes_state_repairrþ   rÿ   rØ   rô   Údb_pathrd   r/   Úget_metar   rÒ   r™   r*   Ú_AUTO_PRUNE_STALE_OPEN_SOURCESrg   rè   Úhermes_state_holdersr
  r  rÙ   r¦   rú   Úset_metar‰   r×   Úinfo)rb   Úretention_daysÚmin_interval_hoursrú   rh   Úmin_vacuum_interval_daysÚmin_vacuum_freelist_ratiorþ   rÿ   ÚresultÚmaintenance_lockr-   Úsince_pruner  r  Úsince_vacuumÚ
vacuum_dueÚratior
  r  ÚholdersrÜ   s                         r   Úmaybe_auto_prune_and_vacuumz3SessionMaintenanceMixin.maybe_auto_prune_and_vacuums  s/  € ÷. 	kØ-2¸aÈ1ÐZ_Ñ!`ˆØÐ! ^°aÒ%7ô �N‰Nðiàô ð !%ˆF�9ÑØˆMÙ=¸d¿l¹lÓKÐØÐ#Ø $ˆF�9ÑØˆMðE	=Ü—)‘)“+ˆCÜ(¨¨d¯m©mÐ<MÓ.NÓOˆKØÐ&¨;Ð9KÈdÑ9RÒ+RØ$(��yÑ!Øñ@ +Ð+;Õ<ôu $ EÐ1FÕGØ(,×(;Ñ(;Ø .¸\Ðgkð )<ó )mð mˆF�8Ñ˜vä# EÐ1FÕGØ×1Ñ1Ü!& ~Ó!6¸Ñ!@Ø×;Ñ;ÈDØ+0ð 2ó ˆFô
  # 6›{ˆF�8Ñô *¨#¨t¯}©}¸]Ó/KÓLˆLØ%¨Ð-Òa°ÐAYÐ\aÑAaÑ1aˆJÙ˜& 1š*©Ø37×3GÑ3GÓ3IÐI�Ð'Ñ(¨5÷Ká3°D·L±LÓAÙ8¸¿¹ÈtÔTñU�áÜ7:¸7³|�FÐ3Ñ4Ü—L‘Lð6ä˜G› d§i¡iÑ/aÐU\Ð]_Ð^_ÐU`Ô/aÓ&aõcð �] eÐ.GÒ&GðJô 0°Ð=SÕTØŸ™œØ-1˜˜zÑ*ØŸ™ m´S¸³XÕ>ô —L‘Lð "Rà!&¨¡Ð0IÈEÑ0QôSð �M‰MÐ+¬S°«XÔ6Ù˜ !šÜ—‘ð Jä ›K¨°ÐPVÐWaÒPbÁÐhjôlñ +Ð+;Ô<Øˆøô% %ò JÜŸ™Ð'CÀS×IÑIûðJûô ò 	'ä�N‰NÐAÀ3ÔGÜ! #›hˆF�7ŽOûð	'ûñ
 +Ð+;Õ<ús\   Á?J( ÂD3J( Ç=I< È	A)J( É<	J%ÊJ ÊJ( Ê J%Ê%J( Ê(	KÊ1$KËK" ËKËK" Ë"
K,r#   )rh   z'Optional[Path]'Úreturnr¥   )rj   r‰   r(  r¢   )r’   r*   rz   úTuple[str, ...]r{   r)  r|   r¢   r}   úOptional[float]r~   r*  r   r¢   r(  z	List[str])rP   zOptional[bool]rQ   r¢   rR   r¢   r(  úTuple[str, list])r(  r+  )NN)r°   r*  r8   r‰   r(  zList[Dict[str, Any]])r°   r*  r8   r‰   r(  r¥   )rÅ   r*   r|   r¢   r(  r¥   )rÈ   NNF)
r°   r*  r8   r‰   rh   úOptional[Path]rÏ   r¢   r(  r¥   )rÚ   r)  rÛ   r‰   r(  zOptional[list])r(  zOptional[int])r(  r*  )rê   r‰   rÛ   r‰   r(  ÚNone)r(  r¥   )r  r¥   r  r¥   rú   r¢   rh   r,  r  r¥   r  r*   r(  zDict[str, Any])Ú__name__Ú
__module__Ú__qualname__Ú__doc__rk   rv   r™   Ústaticmethodr®   r±   r¶   rº   rÀ   rÇ   rÒ   rÝ   rå   rè   rë   rú   r
   r'  r$   r   r   rT   rT   N   s~  „ ÙUô ó0ð FdØ')À%Ø7;Ø=AÐfjñ	9.Ø#(ð9.Ø3Bð9.à$ð9.à;?ð9.ð &5ð9.ð ,;ð	9.ð `dð	9.ð
 
ó9.ðv Ø:>ÐW\Ø6;ñ-Ø/3ð-ØK[ò-ó ð-ó:	Bð
LØ,@ó
Lð`Ø*-ó`ðeØ/2óeð RVõ ð4 SWØ`eðØ%3ðØY]ðà%(óóB	óAó
]ó(óð< %'À"ÐUYØ'+ÈRØ+IðlØ!ðlØ<?ðlØNRðlà$ðlàGJðlð $)ðlð 
ô	lr   rT   )r   r‰   r(  r‰   )r   r‰   r(  zTuple[List[str], list]r#   )r   r‰   )r-   r*   r(  r*  )%r1  Ú
__future__r   Úloggingrd   Úpathlibr   Útypingr   r   r   r   r	   Úhermes_state_commonr
   r   r   r   r   r   Úhermes_startup_watchdogr   Ú	getLoggerrØ   r4   Ú_TOKENS_SQLÚ	_COST_SQLr   r    r(   r/   r‰   r   r    Ú	frozensetr�   rT   r$   r   r   ú<module>r=     s  ðÙ Wå "ã Û Ý ß 3Õ 3÷õ õ <ð 
ˆ×	Ñ	˜>Ó	*€á+¨CÓ0Ð ØL€ØB€	ó.ó"ô:óð ˜9ñ 'ð ð ˜)¡TÐ*:¸WÑ*DÓ%EÐFØ�y¡$Ð'9Ó":Ð;Ø�i¡Ð&9Ó!:Ð;Øˆx™˜nÓ-Ð.Ø�8™TÐ"SÐUZÓ[Ð\Ø�8™TÐ"4Ó5Ð6Ø�8Ð/Ð0Ø�Y¡Ð%;Ó <Ð=Ø�Y¡Ð%;Ó <Ð=Ø�8™TÐ"SÐUZÓ[Ð\Ø�™4Ð MÈsÏyÉyÓYÐZØ�™$˜Ó/Ð0Ø�™$˜Ó/Ð0Ø�(™DÐ!2Ó3Ð4Ø�H™dÐ#YÐ[`ÓaÐbØ�9™d ;°Ñ#8Ó9Ð:Ø�9™d ;°Ñ#8Ó9Ð:Ø�™D ¨WÑ!4Ó5Ð6Ø�™D ¨WÑ!4Ó5Ð6Ø�y¡$Ð'LÓ"MÐNØ�y¡$Ð'LÓ"MÐNð5€ñ8  ÑF°~ÔFÓFÒI|Ñ|Ð ÷Qò Qr   