Ë
    âß²j&) ã                   ó  — 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mZmZm	Z	m
Z
mZmZmZ ddlmZ ddlmZmZ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j@                  d«      Z!de"fd„Z#d	Z$ ejJ                  d
 ejL                  e$«      › d�«      Z' e(h d£«      Z)e)dhz  Z* ejJ                  d«      Z+ ejJ                  d«      Z,dZ-dZ.dZ/dZ0dddœZ1dZ2dZ3de4deejj                     fd„Z6de4ddfd„Z7de8de9fd„Z:de4dee4   fd „Z;de4de4fd!„Z<d"e4dee4   fd#„Z=d$ede4fd%„Z>d&e4d'eddfd(„Z?d)e4d*e4d+ee4   d,e4d-e4de4fd.„Z@ddd/œd+ee4   d0eAd1e9d2eee4      d3eee4      d4eee4      d5ee8   d6ee8   ddfd7„ZB G d8„ d9«      ZCddlDZDddlEZEy):zòFull-text / trigram / CJK message search and FTS maintenance for SessionDB.

Plain mixin for ``hermes_state.SessionDB`` (no ``__init__``/state of its own).
Must never import hermes_state (cycle); shared constants live in hermes_state_common.
é    N)ÚAnyÚCallableÚ
CollectionÚDictÚListÚOptionalÚTuple)Údescribe_skill_invocation)ÚFTS_CJK_STALE_KEYÚFTS_SQLÚFTS_STALE_KEYÚFTS_STORAGE_VERSIONÚFTS_TOOL_CONTENT_PREFIX_CHARSÚFTS_TRIGRAM_EXCLUDED_SOURCESÚFTS_TRIGRAM_SQLÚMAX_FTS5_QUERY_CHARSÚSCHEMA_VERSIONÚ_FTS_CJK_TRIGGERSÚescape_likeÚfts_rebuild_admissionÚfts_trigram_session_sqlÚrouted_sessions_settingÚhermes_stateÚreturnc                  óœ   — t        dd«      } 	 | �t        | «      j                  «       dk(  rdS t        | «      S # t        t
        f$ r Y yw xY w)zU``sessions.search_slow_ms`` for the served profile (default 1000; 0 logs every call).Úsearch_slow_msÚHERMES_SEARCH_SLOW_MSÚ ç     @�@)r   ÚstrÚstripÚfloatÚ	TypeErrorÚ
ValueError)Úvalues    ú8/home/ubuntu/.hermes/hermes-agent/hermes_state_search.pyÚ_search_slow_msr'      sS   € ä#Ð$4Ð6MÓN€EðØ˜¬#¨e«*×*:Ñ*:Ó*<ÀÒ*BˆvÐTÌÈeËÐTøÜ”zÐ"ò Ùðús   Ž9 ®
9 ¹AÁ
Az+{}():"^@/#&|~[]<>,;!?$=\'ú[ú]>   ÚORÚANDÚNOTÚNEARz"[^"]+"|\S+z"[^"]*"zLm.timestamp, m.tool_name, s.source, s.model, s.started_at AS session_startedzCsubstr(m.content, max(1, instr(m.content, ?) - 40), 120) AS snippetz`(m.content LIKE ? ESCAPE '\' OR m.tool_name LIKE ? ESCAPE '\' OR m.tool_calls LIKE ? ESCAPE '\')zŠ(COALESCE(m.content, '') LIKE ? ESCAPE '\' OR COALESCE(m.tool_name, '') LIKE ? ESCAPE '\' OR COALESCE(m.tool_calls, '') LIKE ? ESCAPE '\')zORDER BY m.timestamp DESC, rankzORDER BY m.timestamp ASC, rank)ÚnewestÚoldesta%  WITH target AS (
    SELECT session_id, timestamp, id FROM messages WHERE id IN ({ids})
)
SELECT t.id AS match_id, m.role, m.content
FROM target t JOIN messages m ON m.id IN (
    t.id,
    (SELECT p.id FROM messages p
     WHERE p.session_id = t.session_id AND (p.timestamp, p.id) < (t.timestamp, t.id)
     ORDER BY p.timestamp DESC, p.id DESC LIMIT 1),
    (SELECT n.id FROM messages n
     WHERE n.session_id = t.session_id AND (n.timestamp, n.id) > (t.timestamp, t.id)
     ORDER BY n.timestamp, n.id LIMIT 1)
)
ORDER BY t.id, m.timestamp, m.id))i N  iÿŸ  )i 4  i¿M  )i   iß¦ )i 0  i?0  )i@0  iŸ0  )i 0  iÿ0  )i ¬  i¯×  Úkeyc                 óD   — | j                  d|f«      j                  «       S )z9Point-read one ``state_meta`` row (``None`` when absent).z*SELECT value FROM state_meta WHERE key = ?)ÚexecuteÚfetchone)Úconnr0   s     r&   Ú	_meta_rowr5   P   s   € à�<‰<ÐDÀsÀfÓM×VÑVÓXÐXó    Úkeysc                 ó\   — | j                  ddj                  d„ |D «       «      › d�|«       y )Nz%DELETE FROM state_meta WHERE key IN (ú,c              3   ó    K  — | ]  }d –— Œ y­w©ú?N© ©Ú.0Ú_s     r&   ú	<genexpr>z_delete_meta.<locals>.<genexpr>V   s   è ø€ ÒATÈ!Ä#ÑATùó   ‚ú))r2   Újoin)r4   r7   s     r&   Ú_delete_metarE   U   s,   € Ø‡L�LÐ8¸¿¹ÑATÈtÔATÓ9TÐ8UÐUVÐWÐY]Õ^r6   Úcpc                 ó4   ‡ — t        ˆ fd„t        D «       «      S )Nc              3   óD   •K  — | ]  \  }}|‰cxk  xr |k  nc –— Œ y ­w©Nr=   )r?   ÚloÚhirF   s      €r&   rA   z_is_cjk.<locals>.<genexpr>Z   s   øè ø€ Ò8¡& " bˆr�RŽ~˜2�~ˆ~Ñ8ùs   ƒ )ÚanyÚ_CJK_RANGES)rF   s   `r&   Ú_is_cjkrN   Y   s   ø€ ÜÓ8¬KÔ8Ó8Ð8r6   Ú	raw_queryc                 óv   — | j                  «       D �cg c]  }|j                  «       t        vsŒ|‘Œ c}S c c}w rI   )ÚsplitÚupperÚ_FTS_OPERATORS)rO   Úts     r&   Ú_non_operator_tokensrU   ]   s*   € Ø —‘Ó(ÖL�!¨A¯G©G«I¼^Ò,KŠAÒLÐLùÒLs   “6¯6c                 óN   — dj                  d„ | j                  «       D «       «      S )zYQuote each non-operator token (neutralising FTS5 special characters), keeping AND/OR/NOT.Ú c              3   ó|   K  — | ]4  }|j                  «       t        v r|nd |j                  d d«      z   d z   –— Œ6 y­w)ú"z""N)rR   rS   Úreplace)r?   Útoks     r&   rA   z$_quote_fts_tokens.<locals>.<genexpr>c   s=   è ø€ ò ØY\ˆs�y‰y‹{œnÑ,‰°#¸¿¹ÀCÈÓ8NÑ2NÐQTÑ2TÓTñùs   ‚:<)rD   rQ   )rO   s    r&   Ú_quote_fts_tokensr\   a   s)   € à�8‰8ñ Ø`i×`oÑ`oÓ`qôó ð r6   Útermc                 ó(   — dt        | «      › d�gdz  S )z;One ``%term%`` bind per column of ``_LIKE_ANY_COLUMN_SQL``.ú%é   )Ú_escape_like)r]   s    r&   Ú_like_paramsrb   h   s   € à”˜TÓ"Ð# 1Ð%Ð&¨Ñ*Ð*r6   Údecodedc                 ó2  — t        | t        «      ro| D �cg c]:  }t        |t        «      sŒ|j                  d«      dk(  sŒ)|j                  dd«      ‘Œ< }}dj	                  d„ |D «       «      j                  «       xs dS t        | t        «      r| S dS c c}w )zVMultimodal part list -> joined text (or the placeholder); str passes through; else ''.ÚtypeÚtextr   rW   c              3   ó&   K  — | ]	  }|sŒ|–— Œ y ­wrI   r=   ©r?   rT   s     r&   rA   z _flatten_text.<locals>.<genexpr>q   s   è ø€ Ò.˜aªAœÑ.ùs   ‚Šz[multimodal content])Ú
isinstanceÚlistÚdictÚgetrD   r!   r    )rc   ÚpÚpartss      r&   Ú_flatten_textro   m   s‚   € ä�'œ4Ô Ø,3Ög q´zÀ!ÄTÕ7JÈqÏuÉuÐU[Ë}Ð`fÓOf�—‘�v˜rÕ"ÐgˆÐgØ�x‰xÑ. 5Ô.Ó.×4Ñ4Ó6ÒPÐ:PÐPÜ  ¬#Ô.ˆ7Ð6°BÐ6ùò hs   •B«BÁ BÚnamer%   c                 ó†   — t        |t        «      st        |t        «      st        | › d�«      ‚|dk  rt	        | › d�«      ‚y )Nz must be an integerr   z must be greater than zero)ri   ÚboolÚintr#   r$   )rp   r%   s     r&   Ú_positive_intrt   u   sG   € Ü�%œÔ¤j°¼Ô&<Ü˜4˜&Ð 3Ð4Ó5Ð5Ø�‚zÜ˜D˜6Ð!;Ð<Ó=Ð=ð r6   Úsnippet_sqlÚfrom_sqlÚwhereÚorder_byÚ	limit_sqlc                 óR   — d| › dt         › d|› ddj                  |«      › d|› d|› d�S )zIResult-row SELECT shared by the FTS and LIKE routes (SQL text is pinned).zC
            SELECT m.id, m.session_id, m.role,
                   z,
                   z
            FROM zF
            JOIN sessions s ON s.id = m.session_id
            WHERE ú AND z
            z	
        )Ú_SEARCH_SELECT_TAILrD   )ru   rv   rw   rx   ry   s        r&   Ú_search_select_sqlr}   |   sX   € ðà�=ð !Ü'Ð(ð )Ø�ð à—<‘< Ó&Ð'ð (ØˆJð ØˆKð 	ð	ð 	r6   )Úafter_tsÚ	before_tsÚparamsÚinclude_inactiveÚsource_filterÚexclude_sourcesÚrole_filterr~   r   c                óv  — |s| j                  d«       | j                  d«       |�<| j                  ddj                  d„ |D «       «      › d�«       |j                  |«       |�<| j                  ddj                  d	„ |D «       «      › d�«       |j                  |«       |r<| j                  d
dj                  d„ |D «       «      › d�«       |j                  |«       |�+| j                  d«       |j                  t        |«      «       |�,| j                  d«       |j                  t        |«      «       yy)a„  Append the visibility/source/role/session-start predicates every search route shares. Live
    rows (active=1) AND compaction-archived rows (compacted=1) are discoverable; only
    rewind/undo rows (active=0, compacted=0) are hidden. ``after_ts``/``before_ts`` bound
    ``sessions.started_at`` (inclusive / exclusive) inside the query so LIMIT cannot be
    filled by out-of-window hits.z!(m.active = 1 OR m.compacted = 1)z(COALESCE(m.display_kind, '') <> 'hidden'Nzs.source IN (r9   c              3   ó    K  — | ]  }d –— Œ y­wr;   r=   r>   s     r&   rA   z)_search_filter_clauses.<locals>.<genexpr>˜   s   è ø€ Ò-I°a¬cÑ-IùrB   rC   zs.source NOT IN (c              3   ó    K  — | ]  }d –— Œ y­wr;   r=   r>   s     r&   rA   z)_search_filter_clauses.<locals>.<genexpr>›   s   è ø€ Ò1O¸!´#Ñ1OùrB   zm.role IN (c              3   ó    K  — | ]  }d –— Œ y­wr;   r=   r>   s     r&   rA   z)_search_filter_clauses.<locals>.<genexpr>ž   s   è ø€ Ò+E°A¬CÑ+EùrB   zs.started_at >= ?zs.started_at < ?)ÚappendrD   Úextendrs   )rw   r€   r�   r‚   rƒ   r„   r~   r   s           r&   Ú_search_filter_clausesr‹   Š   s  € ñ Ø�‰Ð8Ô9à	‡L�LÐ;Ô<ØÐ Ø�‰�} S§X¡XÑ-I¸=Ô-IÓ%IÐ$JÈ!ÐLÔMØ�‰�mÔ$ØÐ"Ø�‰Ð(¨¯©Ñ1O¸Ô1OÓ)OÐ(PÐPQÐRÔSØ�‰�oÔ&ÙØ�‰�{ 3§8¡8Ñ+E¸Ô+EÓ#EÐ"FÀaÐHÔIØ�‰�kÔ"ØÐØ�‰Ð(Ô)Ø�‰”c˜(“mÔ$ØÐØ�‰Ð'Ô(Ø�‰”c˜)“nÕ%ð r6   c                   óô  — e Zd ZdZdZedeee      dee	edf      fd„«       Z
dyd„Zdeeeef      fd	„Zdeeeef      fd
„Zdedeeeef      fd„ZdZdZd ed«      › d�Zde› d�Zd ed«      › �Zdyd„Zdyd„Zdedee	eef      ddfd„Zdefd„Zdefd„Zddœdedee   dededef
d„Zdefd „Z dyd!„Z!dyd"„Z"defd#„Z#dyd$„Z$e%defd%„«       Z&e%dyd&„«       Z'dd'œd(ede(fd)„Z)dyd*„Z*defd+„Z+de(fd,„Z,d-eddfd.„Z-defd/„Z.dee   fd0„Z/dd1d2œd3ee0eeef   gdf      d4edeeef   fd5„Z1	 	 dzd6ed7e(d8e(d9e(d:ee	edf      deeef   fd;„Z2	 d{d6ed<e(d=edeeeef      fd>„Z3e%d?edefd@„«       Z4e%dAedefdB„«       Z5e%dAede(fdC„«       Z6e%d?edefdD„«       Z7e%d?edee   fdE„«       Z8e%d?edefdF„«       Z9edGedefdH„«       Z:dGedefdI„Z;d?edefdJ„Z<e%dKedLedMed<e(dNe(de	ee=f   fdO„«       Z>dddPœdKedLedMedQee   dRee   deeeeef         fdS„Z?dTee   dUe=dVedWedeeeef      f
dX„Z@e%d?ede	eee   ee   f   fdY„«       ZAd?ed<e(dNe(dZee   deeeef      f
d[„ZBdyd\„ZC	 d|d]eeeef      d^eee      deeeef      fd_„ZD	 	 	 	 d}d?ed`ee   daee   dbee   d<e(dNe(dZed=edeee      dcee(   ddee(   deeeef      fde„ZE	 	 	 	 d}d?ed`ee   daee   dbee   d<e(dNe(dZed=edeee      dcee(   ddee(   deeeef      fdf„ZFd?edgedheeef   deeeef      fdi„ZGdjed<e(deeeef      fdk„ZH	 	 d~d?ed<e(dledmednee   daee   deeeef      fdo„ZIdpedefdq„ZJdee   fdr„ZKde(fds„ZLde(fdt„ZMdduœdve(dwee(   de(fdx„ZNy)ÚSessionSearchMixinu>   See module docstring â€” mixin for SessionDB (Search cluster).)
ÚidÚ
session_idÚroleÚsnippetÚ	timestampÚ	tool_nameÚsourceÚmodelÚsession_startedÚcontextÚfieldsr   .c                 ó  ‡— |€yt        |t        «      rt        d«      ‚t        |«      Š‰j	                  | j
                  «      }|r&t        ddj                  t        |«      «      › �«      ‚t        ˆfd„| j
                  D «       «      S )z=Validate and canonically order an optional result projection.Nz?search fields must be a collection of field names, not a stringz unknown search result field(s): ú, c              3   ó,   •K  — | ]  }|‰v sŒ|–— Œ y ­wrI   r=   )r?   ÚfieldÚ	requesteds     €r&   rA   z<SessionSearchMixin._search_message_fields.<locals>.<genexpr>º   s   øè ø€ Ò`˜uÈUÐV_ÒM_”UÑ`ùs   ƒ	�)
ri   r    r#   ÚsetÚ
differenceÚ_SEARCH_MESSAGE_RESULT_FIELDSr$   rD   ÚsortedÚtuple)Úclsr˜   Úunknownr�   s      @r&   Ú_search_message_fieldsz)SessionSearchMixin._search_message_fields¯   s~   ø€ ð ˆ>ØÜ�fœcÔ"ÜÐ]Ó^Ð^Ü˜“Kˆ	Ø×&Ñ& s×'HÑ'HÓIˆÙÜÐ?ÀÇ	Á	Ì&ÐQXË/Ó@ZÐ?[Ð\Ó]Ð]ÜÓ`¨×(IÑ(IÔ`Ó`Ð`r6   Nc                 ó°   — | j                   sy	 | j                  | j                  ¬«       y# t        $ r }t        j                  d|«       Y d}~yd}~ww xY w)u  One bounded FTS5 merge pass that never fails the already-committed write (a caller
        must never replay an ambiguous, possibly-durable write â€” even on the bare
        SystemError CPython's sqlite3 layer can raise under cross-thread errmsg scrambling).N)Ú	max_pagesz-FTS incremental merge failed after commit: %s)Ú_fts_enabledÚ_merge_fts_incrementallyÚ_FTS_MERGE_MAX_PAGES_PER_INDEXÚ	ExceptionÚloggerÚwarning)ÚselfÚexcs     r&   Ú_try_incremental_merge_ftsz-SessionSearchMixin._try_incremental_merge_fts¼   sS   € ð × Ò Øð	QØ×)Ñ)°D×4WÑ4WÐ)ÕXøÜò 	Qô
 �N‰NÐJÈC×PÑPûð	Qús   �, ¬	AµAÁAc                 ó$   — | j                  d«      S )zµDeferred-rebuild progress ``{"pending", "total", "indexed", "percent"}`` or None. Reads
        via the pooled reader (not get_meta/self._lock) so search never blocks on the writer.Úfts_rebuild©Ú_rebuild_status©r®   s    r&   Úfts_rebuild_statusz%SessionSearchMixin.fts_rebuild_statusÍ   s   € ð ×#Ñ# MÓ2Ð2r6   c                 ó$   — | j                  d«      S )z:CJK-index backfill progress, or None when none is pending.Úfts_cjk_rebuildr³   rµ   s    r&   Úfts_cjk_rebuild_statusz)SessionSearchMixin.fts_cjk_rebuild_statusÒ   s   € à×#Ñ#Ð$5Ó6Ð6r6   Úprefixc           
      óN  — | j                  d|› d�|› d�f«      }|D �ci c]  }|d   |d   “Œ }}|j                  |› d�«      }|�t        |«      dk  ry t        |«      t        |j                  |› d�«      xs d«      }}d||t        dt        d|z  |z  «      «      d	œS c c}w )
Nz5SELECT key, value FROM state_meta WHERE key IN (?, ?)Ú_high_waterÚ	_progressr0   r%   r   Téd   )ÚpendingÚtotalÚindexedÚpercent)Ú	_read_allrl   rs   Úmin)r®   rº   ÚrowsÚrÚmetaÚ
high_waterrÀ   Úprogresss           r&   r´   z"SessionSearchMixin._rebuild_statusÖ   sË   € Ø�~‰~ÐUØ"( ¨Ð5¸&¸ÀÐ7KÐLóNˆà.2Ö3¨��%‘˜!˜G™*Ñ$Ð3ˆÐ3Ø—X‘X  ¨Ð4Ó5ˆ
ØÐ¤ Z£°AÒ!5ØÜ˜j›/¬3¨t¯x©x¸6¸(À)Ð8LÓ/MÒ/RÐQRÓ+SˆxˆØ¨%¸HÔQTÐUXÔZ]Ð^aÐdlÑ^lÐotÑ^tÓZuÓQvÑwÐwùò 4s   ŸB"zãINSERT INTO {table}(rowid, content, tool_name, tool_calls) SELECT m.id, m.content, m.tool_name, m.tool_calls FROM messages m WHERE m.id > ? AND m.id <= ? {extra}AND NOT EXISTS (SELECT 1 FROM {table}_docsize d WHERE d.id = m.id)a4  INSERT INTO messages_fts(rowid, content, tool_name, tool_calls) SELECT m.id, CASE WHEN m.role = 'tool' THEN substr(COALESCE(m.content, ''), 1, ?) ELSE m.content END, m.tool_name, m.tool_calls FROM messages m WHERE m.id > ? AND m.id <= ? AND NOT EXISTS (SELECT 1 FROM messages_fts_docsize d WHERE d.id = m.id)zÍINSERT INTO messages_fts_trigram(rowid, content, tool_name) SELECT m.id, m.content, m.tool_name FROM messages m JOIN sessions s ON s.id = m.session_id WHERE m.id > ? AND m.id <= ? AND m.role <> 'tool' AND ÚszP AND NOT EXISTS (SELECT 1 FROM messages_fts_trigram_docsize d WHERE d.id = m.id)z„INSERT INTO {table}(rowid, content, tool_name, tool_calls) SELECT id, CASE WHEN role = 'tool' THEN substr(COALESCE(content, ''), 1, zW) ELSE content END, tool_name, tool_calls FROM messages WHERE id > ? AND id <= ?{extra}c                 óÂ   — | j                   dfg}| j                  r|j                  | j                  df«       | j	                  d|«       t
        j                  d«       y)aQ  Finalize the deferred rebuild: boundary sweep + clear markers. The sweep is cheap
        insurance against a write that slipped between high_water capture and trigger
        activation. The trigram half is gated on ``_trigram_available``: without the
        tokenizer/table an unconditional INSERT raises and aborts the whole rebuild.TFr²   u7   Deferred FTS rebuild complete â€” all messages indexed.N)Ú_BASE_BOUNDARY_SWEEP_SQLÚ_trigram_availabler‰   Ú_TRIGRAM_BOUNDARY_SWEEP_SQLÚ_rebuild_finishr¬   Úinfo)r®   Úsweepss     r&   Ú_fts_rebuild_finishz&SessionSearchMixin._fts_rebuild_finish  sT   € ð
 ×0Ñ0°$Ð7Ð8ˆØ×"Ò"Ø�M‰M˜4×;Ñ;¸UÐCÔDØ×Ñ˜]¨FÔ3Ü�‰ÐMÕNr6   c                 ó    — | j                   j                  dd¬«      }| j                  d|dfg«       d| _        t        j                  d«       y)	z?Boundary sweep + clear the cjk markers; index becomes servable.Úmessages_fts_cjkzAND m.role <> 'tool' ©ÚtableÚextrar¸   FTu7   CJK FTS index backfill complete â€” serving CJK search.N)Ú_BOUNDARY_SWEEP_SQLÚformatrÏ   Ú_fts_cjk_availabler¬   rÐ   )r®   Úsweeps     r&   Ú_fts_cjk_rebuild_finishz*SessionSearchMixin._fts_cjk_rebuild_finish  sL   € à×(Ñ(×/Ñ/Ð6HÐPgÐ/ÓhˆØ×ÑÐ.°%¸°Ð0@ÔAØ"&ˆÔÜ�‰ÐMÕNr6   Ú
sweep_sqlsc                 ó6   ‡‡— ˆˆfd„}| j                  |«       y)z¬Sweep a generous window around the high-water boundary, then clear the markers.
        ``(sql, bounded)``: a bounded sweep takes the tool-content prefix_chars param first.c                 óÒ   •— t        | ‰› d�«      }|�Bt        |d   «      }‰D ]/  \  }}|rt        fnd}| j                  |g |¢|dz
  ‘|dz   ‘­«       Œ1 t	        | ‰› d�‰› d�«       y )Nr¼   r   r=   iè  r½   )r5   rs   r   r2   rE   )r4   Úhw_rowÚhwÚsqlÚboundedr€   rº   rÝ   s         €€r&   Ú_doz/SessionSearchMixin._rebuild_finish.<locals>._do  s�   ø€ Ü˜t¨ x¨{Ð%;Ó<ˆFØÐ!Ü˜ ™“^�Ø$.ò G‘L�C˜ÙAHÔ;Ñ=Èb�FØ—L‘L Ð&E¨Ð&E°°T±	Ð&E¸2À¹9Ñ&EÕFðGô ˜ & ¨Ð5¸&¸ÀÐ7KÕLr6   N©Ú_execute_write)r®   rº   rÝ   rä   s    `` r&   rÏ   z"SessionSearchMixin._rebuild_finish  s   ù€ õ	Mð 	×Ñ˜CÕ r6   c                 óæ   — | j                   sy| j                  j                  dd¬«      g}| j                  r|j	                  | j
                  «       | j                  d|d| j                  d¬«      S )	zÔBackfill one chunk of the deferred FTS rebuild; True while work remains. Chunks are
        claimed atomically inside the write transaction, so concurrent processes interleave
        instead of duplicating rows.FÚmessages_ftsr   rÕ   r²   z)FTS rebuild chunk failed (will retry): %sT)Úfail_msgÚfinishÚfinish_when_empty)r¨   Ú_CHUNK_INSERT_SQLrÙ   rÍ   r‰   Ú_TRIGRAM_CHUNK_INSERT_SQLÚ_rebuild_steprÒ   )r®   Úinsertss     r&   Úfts_rebuild_stepz#SessionSearchMixin.fts_rebuild_step$  sv   € ð × Ò ØØ×)Ñ)×0Ñ0°~ÈRÐ0ÓPÐQˆØ×"Ò"Ø�N‰N˜4×9Ñ9Ô:Ø×!Ñ! -°ÐCnØ)-×)AÑ)AÐUYð "ó [ð 	[r6   c                 ó®   — | j                   r| j                  sy| j                  j                  dd¬«      }| j	                  d|g| j
                  d¬«      S )z=Backfill one chunk of the CJK index. True while work remains.FrÔ   z AND role <> 'tool'rÕ   r¸   z-CJK FTS rebuild chunk failed (will retry): %s)rê   ré   )r¨   Ú_fts_cjk_loadedrì   rÙ   rî   rÜ   )r®   Úinserts     r&   Úfts_cjk_rebuild_stepz'SessionSearchMixin.fts_cjk_rebuild_step0  s_   € à× Ò ¨×(<Ò(<ØØ×'Ñ'×.Ñ.Ð5GÐOdÐ.ÓeˆØ×!Ñ!Ð"3°f°XÀd×FbÑFbØ+Zð "ó \ð 	\r6   F)rë   Úinsert_sqlsré   rë   c                ó~  ‡‡‡‡— | j                  ‰› d�«      }|€yt        |«      Š| j                  Šˆˆˆˆfd„}	 | j                  |«      }|du r-| j                  ‰«      }
|r‰dk  s|
�|
d   |
d   k\  r |«        yt        |«      S # t        j
                  $ r }	t        j                  ||	«       Y d}	~	yd}	~	ww xY w)	z³Shared chunk engine for the base and CJK deferred backfills. ``finish_when_empty``
        finalizes a high_water <= 0 marker (empty messages table) instead of leaving it pending.r¼   NFc                 óî   •— t        | ‰› d�«      }|€yt        |d   «      }|‰k\  ryt        |‰z   ‰«      }‰D ]  }| j                  |||f«       Œ | j                  dt	        |«      ‰› d�f«       |‰k  S )Nr½   Fr   z-UPDATE state_meta SET value = ? WHERE key = ?)r5   rs   rÄ   r2   r    )	r4   ÚrowrÉ   rR   râ   ÚchunkrÈ   rõ   rº   s	        €€€€r&   rä   z-SessionSearchMixin._rebuild_step.<locals>._doB  sš   ø€ ô ˜D V H¨IÐ"6Ó7ˆCØˆ{ØÜ˜3˜q™6“{ˆHØ˜:Ò%Øä˜ 5Ñ(¨*Ó5ˆEØ"ò 5�Ø—‘˜S 8¨UÐ"3Õ4ð5ð �L‰LÐHÌ3ÈuË:ÐZ`ÐYaÐajÐWkÐJlÔmØ˜:Ñ%Ð%r6   Tr   rÁ   rÀ   )
Úget_metars   Ú_FTS_REBUILD_CHUNK_ROWSræ   Úsqlite3ÚOperationalErrorr¬   Údebugr´   rr   )r®   rº   rõ   ré   rê   rë   Úhigh_water_rawrä   Úmorer¯   Ústatusrù   rÈ   s    ``        @@r&   rî   z SessionSearchMixin._rebuild_step8  sÅ   û€ ð Ÿ™¨&¨°Ð'=Ó>ˆØÐ!ØÜ˜Ó(ˆ
Ø×,Ñ,ˆ÷	&ð"	Ø×&Ñ& sÓ+ˆDð �5‰=Ø×)Ñ)¨&Ó1ˆFÙ! j°A¢oØÐ" v¨iÑ'8¸FÀ7¹OÒ'Ká”ØÜ�D‹zÐøô ×'Ñ'ò 	Ü�L‰L˜ 3Ô'Üûð	ús   ¼B	 Â	B<ÂB7Â7B<c                 óì  ‡ ‡‡— ‰ j                  «       5 }|j                  d‰ j                  j                  dd«      dz   f«      j	                  «       D �cg c]  }|d   ‘Œ	 }}ddd«       sy|d   Šˆˆ ˆfd„}dd	t
        t           d
t        fˆfd„Š	 t        ‰ j                  |«      «      S c c}w # 1 sw Y   ŒRxY w# t        j                  $ r }t        j                  d|«       Y d}~yd}~ww xY w)uø  Tear down one chunk of a demoted v22 FTS shadow table (a PLAIN table now); True while
        work remains. INTEGER single-column-key tables drain with a high-water marker so
        each chunk's scan is bounded (restarting the scan was O(nÂ²)); compound-key tables
        keep the chunked ``LIMIT`` delete â€” they are small by construction.

        Single-column-key trash tables (the common shape â€” FTS shadow tables carry a rowid/integer PK) are
        drained with a high-water marker mirroring :meth:`fts_rebuild_step`: each chunk deletes only rows
        after the previously-drained key, so the per-chunk scan is bounded instead of re-scanning from the
        start of the table every chunk (O(nÂ²) total on large trash tables, #79324).
        zNSELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE ? ESCAPE '\'r@   z\_r_   r   NFc                 ó  •— | j                  d‰› d�«      D �cg c]'  }|d   dkD  sŒ|d   |d   xs dj                  «       f‘Œ) }}|rdj                  d	„ |D «       «      nd
}t        |«      dk(  rÇ|d   d   dk(  r¼d‰› d�}t	        | |«      }|�t        |d   «      nd}| j                  d|› d‰› d|› d|› d‰j                  › �
|f«      j                  «       }|s	 ‰
| |«      S |d   d   }| j                  d‰› d|› d|› d�||f«      }	|	j                  dkD  r‰j                  |t        |«      | ¬«       y| j                  d‰› d|› d|› d‰› d‰j                  › d�«      }	|	j                  dk(  r ‰
| «      S dS c c}w )NzPRAGMA table_info(rC   é   r   é   é   r   rš   c              3   ó&   K  — | ]	  \  }}|–— Œ y ­wrI   r=   )r?   rp   Ú_typs      r&   rA   zKSessionSearchMixin._fts_teardown_trash_step.<locals>._do.<locals>.<genexpr>w  s   è ø€ Ò;¡Z T¨4œDÑ;ùs   ‚ÚrowidÚINTEGERÚfts_teardown_r½   zSELECT z FROM z WHERE z > ? ORDER BY z LIMIT éÿÿÿÿzDELETE FROM z	 > ? AND z <= ?©ÚcursorTz WHERE (z) IN (SELECT )r2   rR   rD   Úlenr5   rs   rû   ÚfetchallÚrowcountÚset_metar    )r4   rÆ   Úpk_infor0   Ú
marker_keyrø   rÈ   Ú
upper_rowsrR   ÚcurÚ_dropr®   Útbls             €€€r&   rä   z8SessionSearchMixin._fts_teardown_trash_step.<locals>._dou  sÑ  ø€ Ø=A¿\¹\ÐL^Ð_bÐ^cÐcdÐJeÓ=fÖs¸ÐjkÐlmÑjnÐqrÓjr˜˜!™˜q ™tšz r×0Ñ0Ó2Ò3ÐsˆGÐsÙ?F�$—)‘)Ñ;°7Ô;Ô;ÈGˆCÜ�7‹|˜qÒ  W¨Q¡Z°¡]°iÒ%?ð  -¨S¨E°Ð;�
Ü  jÓ1�Ø,/¨OœS  Q¡œ[À�
à!Ÿ\™\Ø˜c˜U &¨¨¨W°S°E¸ÈÀuÈGÐTX×TpÑTpÐSqÐrØ�Mó÷ ‘(“*ð ñ "Ù   zÓ2Ð2Ø" 2™ qÑ)�Ø—l‘l \°#°°g¸c¸UÀ)ÈCÈ5ÐPUÐ#VÐYcÐejÐXkÓl�Ø—<‘< !Ò#Ø—M‘M *¬c°%«jÀ�MÔFØð —,‘,Ø˜s˜e 8¨C¨5°¸c¸UÀ&ÈÈÈWÐUY×UqÑUqÐTrÐrsÐtóˆCð #&§,¡,°!Ò"3‘5˜“;Ð=¸Ð=ùò7 ts
   ™E<§E<r  r   c                 óv   •— | j                  d‰› �«       |�t        | |«       t        j                  d‰«       y)uA   Drained â€” the DROP is cheap now. True: re-check for more trash.zDROP TABLE IF EXISTS z"Old FTS shadow table %s torn down.T)r2   rE   r¬   rÐ   )r4   r  r  s     €r&   r  z:SessionSearchMixin._fts_teardown_trash_step.<locals>._drop“  s9   ø€ à�L‰LÐ0°°Ð6Ô7ØÐ%Ü˜T :Ô.Ü�K‰KÐ<¸cÔBØr6   z0FTS trash teardown chunk failed (will retry): %sTrI   )Ú	_read_ctxr2   Ú_FTS_TRASH_PREFIXrZ   r  r   r    rr   ræ   rü   rý   r¬   rþ   )r®   r4   rÆ   Útrashrä   r¯   r  r  s   `     @@r&   Ú_fts_teardown_trash_stepz+SessionSearchMixin._fts_teardown_trash_stepa  sî   ú€ ð �^‰^Óð 	 Ø#'§<¡<ØaØ×'Ñ'×/Ñ/°°UÓ;¸cÑAÐCó$÷ ‰h‹jö˜a�Q�q“Tð ˆEð ÷	ñ
 ØØ�A‰hˆö	>ñ<	¤H¬S¡Mð 	¼Tõ 	ð	Ü˜×+Ñ+¨CÓ0Ó1Ð1ùò_÷	ð 	ûôb ×'Ñ'ò 	Ü�L‰LÐKÈSÔQÜûð	ús6   ”AB4ÁB/Á!B4ÂC  Â/B4Â4B=Ã C3ÃC.Ã.C3c                 óh   — | j                   syd„ }| j                  |«      r| j                  «        yy)z®From-scratch rebuild of a stale cjk index (triggers were dropped, gap extent unknown):
        drop table + triggers, clear the breadcrumb, recreate (fresh backfill markers).Nc                 óÌ   — t        | t        «      €yt        D ]  }| j                  d|› �«       Œ | j                  d«       | j                  d«       t	        | t        dd«       y)NFzDROP TRIGGER IF EXISTS z%DROP TABLE IF EXISTS messages_fts_cjkz(DROP VIEW IF EXISTS messages_fts_cjk_srcÚfts_cjk_rebuild_high_waterÚfts_cjk_rebuild_progressT)r5   r   r   r2   rE   )r4   Útrigs     r&   rä   z7SessionSearchMixin._fts_cjk_reset_if_stale.<locals>._do§  sd   € Ü˜Ô0Ó1Ð9ØÜ)ò ?�Ø—‘Ð6°t°fÐ=Õ>ð?à�L‰LÐ@ÔAØ�L‰LÐCÔDÜ˜Ô0Ð2NÐPjÔkØr6   )rò   ræ   Ú_ensure_cjk_schema_committed©r®   rä   s     r&   Ú_fts_cjk_reset_if_stalez*SessionSearchMixin._fts_cjk_reset_if_stale¡  s6   € ð ×#Ò#Øò	ð ×Ñ˜sÔ#à×-Ñ-Õ/ð $r6   c                 ó°   — | j                   5  | j                  | j                  «       | j                  j                  «        d d d «       y # 1 sw Y   y xY wrI   )Ú_lockÚ_ensure_fts_cjk_schemaÚ_connÚcommitrµ   s    r&   r#  z/SessionSearchMixin._ensure_cjk_schema_committed´  s@   € Ø�Z‰Zñ 	 Ø×'Ñ'¨¯
©
Ô3Ø�J‰J×ÑÔ÷	 ÷ 	 ñ 	 ús   �6AÁAc                 óÂ   — 	 |j                  d«      j                  «       d   sy|j                  d«      j                  «       d    S # t        j                  $ r Y yw xY w)a  True when the base FTS table indexes nothing while ``messages`` has rows (post-demote
        empty-index shape). Caller holds ``self._lock``. docsize is the authoritative "is this
        rowid indexed" surface; EXISTS not COUNT(*) because this runs on every writable open.z%SELECT EXISTS(SELECT 1 FROM messages)r   Fz1SELECT EXISTS(SELECT 1 FROM messages_fts_docsize))r2   r3   rü   rý   ©r®   r4   s     r&   Ú'_fts_external_index_empty_with_messagesz:SessionSearchMixin._fts_external_index_empty_with_messages¹  s`   € ð	Ø—<‘<Ð GÓH×QÑQÓSÐTUÒVØØ—|‘|Ð$WÓX×aÑaÓcÐdeÑfÐfÐføÜ×'Ñ'ò 	Ùð	ús   ‚"A ¥"A ÁAÁAc                 óŠ   — t        |d«      €7| j                  |«      s| j                  |«       | j                  dd|¬«       yy)aØ  high_water without progress: fts_rebuild_step reads missing progress as "done by
        another process" and optimize would no-op then stamp. Reset to known-empty, re-seed.
        Truncation goes through FTS5 ``'delete-all'`` (a plain DELETE is O(rows) and corrupts
        the index when indexed rows diverged from ``messages``); the backfill worker replays
        without an anti-join, so it needs a known-empty index. A missing docsize table counts
        as empty.Úfts_rebuild_progressNÚ0r  )r5   Ú_fts_index_known_emptyÚ_reset_fts_index_to_emptyr  r,  s     r&   Ú_reseed_missing_progressz+SessionSearchMixin._reseed_missing_progressÄ  sF   € ô �TÐ1Ó2Ð:Ø×.Ñ.¨tÔ4Ø×.Ñ.¨tÔ4Ø�M‰MÐ0°#¸dˆMÕCð ;r6   c                 ó’   — 	 t        | j                  d«      j                  «       d   «      dk(  S # t        j                  $ r Y yw xY w)zZTrue when the base external-content index holds no rows (a missing table counts as empty).z)SELECT COUNT(*) FROM messages_fts_docsizer   T)rs   r2   r3   rü   rý   )r4   s    r&   r1  z)SessionSearchMixin._fts_index_known_emptyÐ  sI   € ð	Ü�t—|‘|Ð$OÓP×YÑYÓ[Ð\]Ñ^Ó_ÐcdÑdÐdøÜ×'Ñ'ò 	Ùð	ús   ‚-0 °AÁAc           	      ó²   — dD ]F  }t        j                  t        j                  «      5  | j	                  d|› d|› d�«       ddd«       ŒH y# 1 sw Y   ŒSxY w)z±Truncate the v23 external-content tables via FTS5 ``'delete-all'`` (O(1); a plain DELETE is
        O(rows) and corrupts the index when indexed rows diverged from ``messages``).)rè   Úmessages_fts_trigramúINSERT INTO ú(z) VALUES('delete-all')N)Ú
contextlibÚsuppressrü   rý   r2   )r4   r  s     r&   r2  z,SessionSearchMixin._reset_fts_index_to_emptyØ  sd   € ð <ò 	OˆCÜ×$Ñ$¤W×%=Ñ%=Ó>ñ OØ—‘˜|¨C¨5°°#°Ð6LÐMÔN÷Oð Oñ	O÷Oð Oús   ©AÁA	©Úforcer<  c                ó  — t        |d«      }|�!|s| j                  |«       t        |d   «      S |j                  d«      j	                  «       d   }| j                  dt        |«      |¬«       | j                  dd|¬«       t        |«      S )zóWrite ``fts_rebuild_high_water`` / ``fts_rebuild_progress`` for a full backfill; returns
        the high-water id. Without ``force`` an existing high_water only gets a missing
        progress key repaired. Caller holds the write transaction.Úfts_rebuild_high_waterr   z)SELECT COALESCE(MAX(id), 0) FROM messagesr  r/  r0  )r5   r3  rs   r2   r3   r  r    )r®   r4   r<  Úexisting_hwrá   s        r&   Ú_seed_fts_rebuild_markersz,SessionSearchMixin._seed_fts_rebuild_markersà  s‡   € ô   Ð&>Ó?ˆØÐ"©5Ø×)Ñ)¨$Ô/Ü�{ 1‘~Ó&Ð&Ø�\‰\ÐEÓF×OÑOÓQÐRSÑTˆØ�‰Ð.´°B³ÀˆÔEØ�‰Ð,¨c¸$ˆÔ?Ü�2‹wˆr6   c                 ó2   ‡ — ˆ fd„}‰ j                  |«       y)u“  Heal interrupted demote/backfill bookkeeping before optimize runs: orphan high_water
        gets progress re-seeded; an empty external index with messages and no markers gets
        a full backfill claim. Never invents markers on a still-legacy inline DB â€” optimize
        would then skip demote and INSERT against the inline table forever.

        Covers two post-#65798 failure classes:
        c                 óÈ   •— t        | d«      �‰j                  | «       y ‰j                  | «      ry ‰j                  | «      r t	        | d«       ‰j                  | d¬«       y y )Nr>  Úfts_storage_versionTr;  )r5   r3  Ú_db_has_legacy_inline_ftsr-  rE   r@  )r4   r®   s    €r&   rä   z<SessionSearchMixin._repair_optimize_bookkeeping.<locals>._doõ  se   ø€ Ü˜Ð7Ó8ÐDØ×-Ñ-¨dÔ3ØØ×-Ñ-¨dÔ3ØØ×;Ñ;¸DÔAÜ˜TÐ#8Ô9Ø×.Ñ.¨t¸4Ð.Õ@ð Br6   Nrå   r$  s   ` r&   Ú_repair_optimize_bookkeepingz/SessionSearchMixin._repair_optimize_bookkeepingí  s   ø€ ô	Að 	×Ñ˜CÕ r6   c                 ót  — | j                   r| j                  ry| j                  «       5 }| j                  |«      xsf t	        |d«      duxsV | j
                  xr" t	        |d«      duxs t	        |t        «      duxs$ | j                  |«      xs | j                  |«      	 cddd«       S # 1 sw Y   yxY w)aN  True when `optimize_fts_storage()` has work: legacy inline FTS or a v23 trigram still
        carrying ``tool_calls`` (``_db_needs_fts_storage_upgrade``), an interrupted optimize
        (markers/trash), a CJK backfill on this tokenizer-capable host, or an empty external
        index without markers. False when FTS5 is unavailable.Fr>  Nr   )	r¨   Ú	read_onlyr  Ú_db_needs_fts_storage_upgrader5   rò   r   Ú_has_fts_trashr-  r,  s     r&   Úfts_optimize_availablez)SessionSearchMixin.fts_optimize_available   sÈ   € ð
 × Ò  D§N¢NØØ�^‰^Óð 	 à×2Ñ2°4Ó8ò FÜ˜TÐ#;Ó<ÀDÐHòFð ×(Ñ(ò Ü˜dÐ$@ÓAÈÐMò FÜ  Ô'8Ó9ÀÐEòFð ×&Ñ& tÓ,òFð ×?Ñ?ÀÓEð
÷	÷ 	ò 	ús   ªA:B.Â.B7c                 óh   ‡ — ˆ fd„}t        ‰ j                  |«      «      }‰ j                  d«       |S )u°  Demote upgrade-eligible FTS vtables and stage their shadow tables for chunked
        teardown; returns MAX(messages.id) as the rebuild high water. O(1) schema surgery
        â€” the heavy delete is deferred. Markers land in the same BEGIN IMMEDIATE, BEFORE
        the empty v23 schema is created (``executescript`` implicitly COMMITs), closing
        the crash window where trash + empty v23 tables exist with no backfill claim.c                 ó°  •— ‰j                  | «       | j                  d«       | j                  d«      j                  «       rt| j                  d«       | j                  d«       | j                  d«       | j                  d«      j                  «       D ]  }| j                  d|d   › d	|d   › �«       Œ! ‰j	                  | d
¬«      }t        | d«       |S )Nz,DROP VIEW IF EXISTS messages_fts_trigram_srcz’SELECT 1 FROM sqlite_master WHERE type = 'table' AND name IN ('messages_fts', 'messages_fts_trigram') AND sql LIKE 'CREATE VIRTUAL TABLE%' LIMIT 1zPRAGMA writable_schema=ONzˆDELETE FROM sqlite_master WHERE type = 'table' AND name IN ('messages_fts', 'messages_fts_trigram') AND sql LIKE 'CREATE VIRTUAL TABLE%'zPRAGMA writable_schema=RESETzÃSELECT name FROM sqlite_master WHERE type = 'table' AND (name LIKE 'messages_fts_%' ESCAPE '\' OR name LIKE 'messages_fts_trigram_%' ESCAPE '\') AND name NOT LIKE 'messages\_fts\_cjk%' ESCAPE '\'zALTER TABLE r   z RENAME TO fts_v22_trash_Tr;  rJ  )Ú_drop_fts_triggersr2   r3   r  r@  rE   )r4   rø   rá   r®   s      €r&   Ú_stagez>SessionSearchMixin._demote_legacy_fts_to_trash.<locals>._stage  så   ø€ Ø×#Ñ# DÔ)Ø�L‰LÐGÔHØ�|‰|ð?ó÷ ‰h‹jðð —‘Ð8Ô9Ø—‘ðpôð —‘Ð;Ô<ØŸ<™<ðLó
÷ ‘(“*ò[�Cð —L‘L <°°A±¨xÐ7PÐQTÐUVÑQWÐPXÐ!YÕZð[ð ×/Ñ/°¸DÐ/ÓAˆBÜ˜Ð7Ô8ØˆIr6   z@failed to create v23 messages_fts during optimize-storage demote)rs   ræ   Ú_ensure_v23_fts_tables)r®   rN  rá   s   `  r&   Ú_demote_legacy_fts_to_trashz.SessionSearchMixin._demote_legacy_fts_to_trash  s3   ø€ ô	ô> �×$Ñ$ VÓ,Ó-ˆà×#Ñ#Ð$fÔgØˆ	r6   Úfailure_messagec                 óL  — | j                   5  | j                  | j                  dt        «      }| j                  | j                  dt        «      }t        |«      | _        |st        j                  |«      ‚| j                  j                  «        ddd«       y# 1 sw Y   yxY w)z«Ensure the v23 base + trigram tables under the lock (IF NOT EXISTS, cheap); raise
        *failure_message* without the base table (the backfill loop would retry forever).rè   r6  N)
r'  Ú_ensure_fts_schemar)  r   r   rr   rÍ   rü   rý   r*  )r®   rQ  Úbase_okÚ
trigram_oks       r&   rO  z)SessionSearchMixin._ensure_v23_fts_tables>  sƒ   € ð �Z‰Zñ 	 Ø×-Ñ-¨d¯j©j¸.Ì'ÓRˆGØ×0Ñ0°·±Ð=SÔUdÓeˆJÜ&*¨:Ó&6ˆDÔ#ÙÜ×.Ñ.¨Ó?Ð?Ø�J‰J×ÑÔ÷	 ÷ 	 ñ 	 ús   �BBÂB#c                 óÊ  — 	 | j                   5  | j                  j                  d«       ddd«       d}	 | j                   5  | j                  j                  d«       ddd«       |S # 1 sw Y   Œ>xY w# t        j                  $ r"}t
        j                  d|«       d}Y d}~Œod}~ww xY w# 1 sw Y   |S xY w# t        $ r!}t
        j                  d|«       Y d}~|S d}~ww xY w)z‹Phase 3: reclaim freed pages to the OS. False when VACUUM failed (usually no free disk
        for its temp copy; a later VACUUM reclaims).ÚVACUUMNTz$VACUUM after FTS optimize failed: %sFzPRAGMA wal_checkpoint(PASSIVE)z9WAL checkpoint (PASSIVE) after optimize VACUUM failed: %s)	r'  r)  r2   rü   rý   r¬   r­   r«   rþ   )r®   Ú	vacuum_okr¯   s      r&   Ú_optimize_vacuumz#SessionSearchMixin._optimize_vacuumI  sÖ   € ð	Ø—‘ñ -Ø—
‘
×"Ñ" 8Ô,÷-àˆIð	[Ø—‘ñ Eð
 —
‘
×"Ñ"Ð#CÔD÷Eð Ð÷%-ð -ûô ×'Ñ'ò 	Ü�N‰NÐAÀ3ÔGØ�Iûð	ú÷Eð Ðûô ò 	[Ü�L‰LÐTÐVY×ZÐZØÐûð	[úsb   ‚A3 ŽA'ª
A3 µB8 ÁB+ÁB8 Á'A0Á,A3 Á3B(ÂB#Â#B(Â+B5Â0B8 Â5B8 Â8	C"ÃCÃC"c                 óø   — t        |d«      �y| j                  |«      ry| j                  |«      ry| j                  dt	        t
        «      |¬«       t        |d«       |j                  dt        t        f«       y)	aµ  Phase 4 (inside the write transaction, so a concurrent writer cannot race a stamp past
        incomplete work): stamp the FTS layout (source of truth for "optimized"), clear the
        "available" flag, advance a lagging schema_version. Returns a refusal reason or None.
        Refuses while optimize work remains; an empty base index against non-empty messages
        also refuses (settling there meant permanent search-index loss).r>  NÚbackfill_incompleteÚteardown_incompleterC  r  rJ  z7UPDATE schema_version SET version = ? WHERE version < ?)	r5   rI  r-  r  r    r   rE   r2   r   r,  s     r&   Ú_optimize_settlez#SessionSearchMixin._optimize_settlea  st   € ô �TÐ3Ó4Ð@Ø(Ø×Ñ˜tÔ$Ø(Ø×7Ñ7¸Ô=Ø(Ø�‰Ð+¬SÔ1DÓ-EÈdˆÔSÜ�TÐ3Ô4Ø�‰ÐNÔQ_ÔaoÐPpÔqØr6   T)Úprogress_cbÚvacuumr^  r_  c                ó’  ‡ ‡‡— ‰ j                   sdddœS ‰ j                  rdddœS ‰ j                  «        ‰ j                  5  ‰ j	                  ‰ j
                  «      }ddd«       ‰ j                  d«      du}r|s‰ j                  «        n|r|s‰ j                  d«       ‰ j                  «        ‰ j                  r‰ j                  «        dt        d	dfˆˆ fd
„Šdt        d	dfˆˆ fd„} ‰d«        |d‰ j                  «        ‰d«        |d‰ j                  «        ‰d«        |d‰ j                  «       ‰ j!                  «       5 }t#        |d«      du}‰ j%                  |«      }‰ j'                  |«      }	ddd«       ss	r'|s	rdnd}
t(        j+                  d|
|	«       d|
ddœS d}|r ‰d«       ‰ j-                  «       }‰ j/                  ‰ j0                  «      }|�t(        j+                  d|«       d||dœS  ‰d«       t(        j3                  dt4        «       d|dœS # 1 sw Y   �ŒÔxY w# 1 sw Y   Œ¿xY w)aq  Repair an older FTS layout into the current v23 shape, foreground and to completion:
        legacy-v22 inline -> external-content, or a v23 ``messages_fts_trigram`` that still stores
        ``tool_calls``. Re-running resumes. ``progress_cb`` receives {"phase", "percent",
        "indexed", "total"}. A missing trigram tokenizer is not fatal (CJK falls back to LIKE).FÚfts5_unavailable)ÚokÚreasonrG  Nr>  z?failed to re-create v23 messages_fts on optimize-storage resumeÚphaser   c                 ó”   •— ‰€y ‰j                  «       xs ‰j                  «       } ‰| |r|d   nd|r|d   nd|r|d   nddœ«       y )NrÂ   r¾   rÁ   r   rÀ   )rd  rÂ   rÁ   rÀ   )r¶   r¹   )rd  Ústr^  r®   s     €€r&   Ú_emitz6SessionSearchMixin.optimize_fts_storage.<locals>._emit�  sZ   ø€ ØÐ"ØØ×(Ñ(Ó*ÒK¨d×.IÑ.IÓ.KˆBÙ %ÁR°B°y²MÈSÙ57 B y¢M¸QÑXZÈÈGÊÐ`añcõ dr6   c                 óæ   •— 	 t        j                  «       } |«       sy ‰| «       t        j                  t        ‰j                  t        j                  «       |z
  ‰j
                  z  «      «       Œp)uÑ   Run *step* to completion; the inter-chunk sleep is the single place the duty
            cycle is enforced â€” back-to-back BEGIN IMMEDIATE chunks starve a live
            gateway/CLI out of its lock retries.N)ÚtimeÚ	monotonicÚsleepÚmaxÚ_FTS_REBUILD_MIN_PAUSEÚ_FTS_REBUILD_DUTY_FACTOR)rd  ÚstepÚ_t0rg  r®   s      €€r&   Ú_drivez7SessionSearchMixin.optimize_fts_storage.<locals>._drive—  sZ   ø€ ð Ü—n‘nÓ&�Ù”vØÙ�e”Ü—
‘
œ3˜t×:Ñ:¼T¿^¹^Ó=MÐPSÑ=SÐW[×WtÑWtÑ<tÓuÔvð r6   ÚbackfillÚteardownr[  r\  zPFTS storage optimization did not settle (%s): pending=%s trash=%s empty_index=%s)rb  rc  Úvacuumedr_  z,FTS storage optimization settle refused (%s)Údonez/FTS storage optimization complete (layout v%d).T)rb  rt  )r¨   rG  rE  r'  rH  r)  rú   rP  rO  r%  rò   r#  r    rð   rô   r  r  r5   rI  r-  r¬   r­   rY  ræ   r]  rÐ   r   )r®   r^  r_  Úneeds_storage_upgrader¿   rq  r4   Ústill_pendingÚstill_trashÚempty_indexrc  rX  Úrefusalrg  s   ``           @r&   Úoptimize_fts_storagez'SessionSearchMixin.optimize_fts_storager  sP  ú€ ð × Ò ØÐ+=Ñ>Ð>Ø�>Š>Ø¨;Ñ7Ð7ð 	×)Ñ)Ô+Ø�Z‰Zñ 	SØ$(×$FÑ$FÀtÇzÁzÓ$RÐ!÷	Sà—-‘-Ð 8Ó9ÀÐEˆÙ ©Ø×,Ñ,Õ.ÙÑ2ð ×'Ñ'Ð(iÔjð 	×$Ñ$Ô&Ø×ÒØ×-Ñ-Ô/ð	dœð 	d ö 	dð		wœ#ð 		w¨ö 		wñ 	ˆjÔÙˆz˜4×0Ñ0Ô1ÙˆjÔÙˆz˜4×4Ñ4Ô5áˆjÔÙˆz˜4×8Ñ8Ô9Ø�^‰^Óð 	M Ü% dÐ,DÓEÈTÐQˆMØ×-Ñ-¨dÓ3ˆKØ×FÑFÀtÓLˆK÷	Mñ ™K©;Ù.;¹{Ñ*ÐPeˆFÜ�N‰NÐmØ! =°+¸{ôLà¨6¸tÑDÐDàˆ	ÙÙ�(ŒOØ×-Ñ-Ó/ˆIØ×%Ñ% d×&;Ñ&;Ó<ˆØÐä�N‰NÐIÈ7ÔSØ¨7À	ÑJÐJÙˆfŒÜ�‰ÐEÔGZÔ[Ø¨	Ñ2Ð2÷	Sñ 	Sú÷T	Mð 	Mús   ÁH0Å1H=È0H:È=Ir�   Úaround_message_idÚwindowÚbookendÚ
keep_rolesc           
      ó
  ‡ ‡‡‡‡‡— t        ‰d«      Š‰ j                  ‰||¬«      }|d   }|sg ddg g dœS |}|�Bt        |«      }	|D �
cg c],  }
|
j                  d«      |k(  s|
j                  d«      |	v sŒ+|
‘Œ. }}
g }g }‰dkD  r–|€dnd	d
j	                  d„ |D «       «      › d�Š|€g n
t        |«      Š‰ j                  «       5 Šdt        dt        dt        fˆˆˆˆˆfd„} |d|d   d   d«      }t        t         |d|d   d   d«      «      «      }ddd«       dt        t        t        f   fˆ fd„}||d   |d   |D �cg c]
  } ||«      ‘Œ c}|D �cg c]
  } ||«      ‘Œ c}dœS c c}
w # 1 sw Y   Œ]xY wc c}w c c}w )aÔ  Anchored window (``get_messages_around``) plus session bookends, so one call yields the
        goal and the resolution of a long session. ``window`` is filtered to ``keep_roles``
        (None disables) EXCEPT the anchor; ``bookend_start`` / ``bookend_end`` are the
        first/last ``bookend`` non-empty-content messages with ids strictly outside the
        window (empty when it overlaps the head/tail). Empty result when the anchor isn't
        in the session.r   )r}  r}  )r}  Úmessages_beforeÚmessages_afterÚbookend_startÚbookend_endNrŽ   r�   r   z AND role IN (r9   c              3   ó    K  — | ]  }d –— Œ y­wr;   r=   r>   s     r&   rA   z7SessionSearchMixin.get_anchored_view.<locals>.<genexpr>Ù  s   è ø€ ÒQjÐZ[ÔRUÑQjùrB   rC   ÚopÚboundary_idÚorderc           	      óf   •— ‰j                  d| › d‰› d|› d�‰|g‰¢‰‘­«      j                  «       S )Nz3SELECT * FROM messages WHERE session_id = ? AND id z ?z% AND length(content) > 0 ORDER BY id z LIMIT ?)r2   r  )r†  r‡  rˆ  r~  r4   Úrole_clauseÚrole_paramsr�   s      €€€€€r&   Ú_bookendz6SessionSearchMixin.get_anchored_view.<locals>._bookendÜ  sS   ø€ ØŸ<™<ð7Ø79°d¸"¸[¸Mð J'à', g¨Xð7ð $ [ÐH°;ÐHÀÑHó÷ ‘h“jð!r6   Ú<ÚASCÚ>r  ÚDESCr   c                 ó,   •— ‰j                  | dd¬«      S )NÚget_anchored_viewF)Úwarn_contextÚsummary_flag)Ú_row_to_message_dict)rø   r®   s    €r&   Ú_hydratez6SessionSearchMixin.get_anchored_view.<locals>._hydrateè  s   ø€ Ø×,Ñ,¨SÐ?RÐafÐ,ÓgÐgr6   r�  r‚  )rl  Úget_messages_aroundrž   rl   rD   rj   r  r    rs   Úreversedr   r   )r®   r�   r|  r}  r~  r  Ú	primitiveÚwindow_rowsÚfiltered_windowÚkeep_setÚmÚbookend_start_rowsÚbookend_end_rowsrŒ  r–  rÆ   r4   rŠ  r‹  s   ``  `           @@@r&   r’  z$SessionSearchMixin.get_anchored_viewÃ  sÉ  ý€ ô �g˜q“/ˆØ×,Ñ,¨ZÐ9JÐSYÐ,ÓZˆ	Ø Ñ)ˆÙØ °QÈ!Ð^`ÐqsÑtÐtà%ˆØÐ!Ü˜:“ˆHØ*5Öw Q¸¿¹¸t»ÐHYÒ9YÐ]^×]bÑ]bÐciÓ]jÐnvÒ]všqÐwˆOÐwØ(*ÐØ&(ÐØ�QŠ;Ø *Ð 2™"¸.ÈÏÉÑQjÐ_iÔQjÓIjÐHkÐklÐ8mˆKØ *Ð 2™"¼¸ZÓ8HˆKØ—‘Ó!ð ` Tð!¤ð !´3ð !¼s÷ !ñ !ñ &.¨c°;¸q±>À$Ñ3GÈÓ%OÐ"ä#'¬±¸#¸{È2¹ÈtÑ?TÐV\Ó1]Ó(^Ó#_Ð ÷`ð	hœT¤#¤s (™^õ 	hð &¸)ÐDUÑ:VØ'Ð(8Ñ9Ø3EÖF¨a™h q�kÒFØ1AÖB¨A™H Q�KÒBñ	
ð 	
ùò+ x÷`ð `üò$ GùÚBs%   Á	,E*Á6E*ÃAE/ÅE;
ÅF Å/E8Úlimitr�   c                 ób  — |rdnd}d}| j                  «       5 }|j                  d|› |› d�|t        |«      dz  dz   f«      j                  «       }ddd«       d	d
lm} g }	D ]½  }
t        |	«      t        |«      k\  r |	S | j                  |
d   «      }|j                  |«      rŒCt        |t        «      rt        |«      xs |}nt        |«      }dj                  |j                  «       «      }t        |«      dkD  r|dd dz   }|	j                  |
d   |
d   |dœ«       Œ¿ |	S # 1 sw Y   ŒÕxY w)uù  The *limit* most-recent real user turns, newest first, as ``{id, timestamp, preview}``
        (80 chars, whitespace collapsed); used by /rewind and ``/undo [N]``. Bookkeeping rows
        (``display_kind`` set) are excluded. Legacy compaction handoffs are role='user' rows
        with NO display_kind â€” invisible to SQL â€” so fetch with headroom and drop them in the
        decode loop; otherwise ``/undo N`` pairs an in-memory count that excludes handoffs
        with a DB pick that includes them.r   z AND active = 1zJ AND (display_kind IS NULL OR display_kind = '' OR display_kind = 'steer')zRSELECT id, timestamp, content FROM messages WHERE session_id = ? AND role = 'user'z ORDER BY id DESC LIMIT ?r  r  Nr   )ÚContextCompressorÚcontentrW   éP   éM   z...rŽ   r’   )rŽ   r’   Úpreview)r  r2   rs   r  Úagent.context_compressorr¢  r  Ú_decode_contentÚ_is_context_summary_contentri   r    r
   ro   rD   rQ   r‰   )r®   r�   r   r�   Úactive_clauseÚdisplay_clauser4   rÅ   r¢  Úresultrø   rc   r¦  s                r&   Úlist_recent_user_messagesz,SessionSearchMixin.list_recent_user_messagesñ  sR  € ñ /™Ð4Eˆð fˆØ�^‰^Óð 	 Ø—<‘<ØdØ �/ .Ð!1ð 2+ð+ð œS ›Z¨!™^¨aÑ/Ð0ó	÷
 ‰h‹jð ÷	õ 	?Ø')ˆØò 	`ˆCÜ�6‹{œc %›jÒ(Øð ˆð ×*Ñ*¨3¨y©>Ó:ˆGØ ×<Ñ<¸WÔEØÜ˜'¤3Ô'Ü3°GÓ<ÒGÀ‘ä'¨Ó0�Ø—h‘h˜wŸ}™}›Ó/ˆGÜ�7‹|˜bÒ Ø! # 2˜,¨Ñ.�Ø�M‰M  T¡¸¸[Ñ9IÐV]Ñ^Õ_ð	`ð ˆ÷/	ð 	ús   ™8D%Ä%D.Úqueryc                 ó”  ‡— | dt          } g Šdddt        fˆfd„}t        j                  || «      j	                  dd«      }t
        j                  d|«      }d|v r't        j                  |«      s|j	                  dd«      }t        j                  d	d
|«      }t        j                  dd|«      }t        j                  dd|j                  «       «      }t        j                  dd|j                  «       «      }t        j                  dd|«      }t        ‰«      D ]  \  }}|j	                  d|› d�|«      }Œ |j                  «       S )a	  Sanitize user input for FTS5 MATCH (raw special characters raise): preserve paired
        quoted phrases, strip unmatched special characters, and quote hyphenated/dotted
        terms so FTS5 matches them as phrases (``chat-send``, ``P2.2``, ``my-app.config.ts``).Nr�  zre.Match[str]r   c                 óh   •— ‰j                  | j                  d«      «       dt        ‰«      dz
  › d�S )Nr   ú Qr  ú )r‰   Úgroupr  )r�  Ú_quoted_partss    €r&   Ú_holdz6SessionSearchMixin._sanitize_fts5_query.<locals>._hold$  s3   ø€ Ø× Ñ  §¡¨£Ô,Øœ3˜}Ó-°Ñ1Ð2°$Ð7Ð7r6   rY   rW   r_   z\*+Ú*z(^|\s)\*z\1z(?i)^(AND|OR|NOT)\b\s*r   z(?i)\s+(AND|OR|NOT)\s*$z\b(\w+(?:[._-]\w+)+)\bz"\1"r±  r²  )r   r    Ú_QUOTED_PHRASE_REÚsubrZ   Ú_FTS5_SPECIAL_REr�   Ú_contains_cjkÚrer!   Ú	enumerate)r®  rµ  Ú	sanitizedÚiÚquotedr´  s        @r&   Ú_sanitize_fts5_queryz'SessionSearchMixin._sanitize_fts5_query  s8  ø€ ð Ð+Ô+Ð,ˆð !ˆð	8�_ð 	8¬õ 	8ô &×)Ñ)¨%°Ó7×?Ñ?ÀÀSÓIˆ	ô %×(Ñ(¨¨iÓ8ˆ	Ø�)ÑÔ$6×$DÑ$DÀYÔ$OØ!×)Ñ)¨#¨sÓ3ˆIä—F‘F˜6 3¨	Ó2ˆ	Ü—F‘F˜;¨¨yÓ9ˆ	ä—F‘FÐ4°b¸)¿/¹/Ó:KÓLˆ	Ü—F‘FÐ5°r¸9¿?¹?Ó;LÓMˆ	ô —F‘FÐ4°g¸yÓIˆ	ä" =Ó1ò 	C‰IˆAˆvØ!×)Ñ)¨E°!°°D¨/¸6ÓB‰Ið	Cà�‰Ó Ð r6   rf   c                 ó&   — t        d„ | D «       «      S )Nc              3   óD   K  — | ]  }t        t        |«      «      –— Œ y ­wrI   ©rN   Úord©r?   Úchs     r&   rA   z3SessionSearchMixin._contains_cjk.<locals>.<genexpr>?  s   è ø€ Ò3¨”7œ3˜r›7×#Ñ3ùs   ‚ )rL   ©rf   s    r&   rº  z SessionSearchMixin._contains_cjk=  s   € äÑ3¨dÔ3Ó3Ð3r6   c                 ó&   — t        d„ | D «       «      S )Nc              3   óJ   K  — | ]  }t        t        |«      «      sŒd –— Œ y­w)r  NrÃ  rÅ  s     r&   rA   z0SessionSearchMixin._count_cjk.<locals>.<genexpr>C  s   è ø€ Ò8˜¤w¬s°2«wÕ'7”1Ñ8ùs   ‚#œ#)ÚsumrÇ  s    r&   Ú
_count_cjkzSessionSearchMixin._count_cjkA  s   € äÑ8˜tÔ8Ó8Ð8r6   c                 ód   — d}| D ]%  }t        t        |«      «      r|dz  }Œ|dk(  r yd}Œ' |dk(  S )u²   True when any maximal CJK run is a single char: the cjk-bigram index stores unigrams
        only for isolated chars, so such a term can't match inside longer runs â€” keep LIKE.r   r  TrÃ  )r®  ÚrunrÆ  s      r&   Ú_has_lone_cjk_runz$SessionSearchMixin._has_lone_cjk_runE  sG   € ð ˆØò 	ˆBÜ”s˜2“wÔØ�q‘‘à˜!’8ÙØ‘ð	ð �a‰xˆr6   c                 óÖ   — g }t         j                  | «      D ]/  }|j                  «       }|dv r y|dk7  sŒ|j                  |«       Œ1 t	        |«      dk\  rdj                  |«      S dS )ag  The sanitized implicit-AND query rewritten as an any-term OR query, or ``None`` when
        relaxation does not apply: fewer than two searchable units (a single term cannot relax)
        or explicit ``OR``/``NOT`` (the caller expressed exact semantics). Quoted phrases stay
        whole units: ``"docker networking" tls`` -> ``"docker networking" OR tls``.>   r*   r,   Nr+   r  ú OR )Ú_LIKE_TOKEN_REÚfindallrR   r‰   r  rD   )r®  ÚunitsÚ	raw_tokenrR   s       r&   Ú_or_relaxed_queryz$SessionSearchMixin._or_relaxed_queryS  sm   € ð ˆÜ'×/Ñ/°Ó6ò 	(ˆIØ—O‘OÓ%ˆEØ˜Ñ%ÙØ˜‹~Ø—‘˜YÕ'ð	(ô &)¨£Z°1¢_ˆv�{‰{˜5Ó!Ð>¸$Ð>r6   c                 ó�   — t        | j                  d«      j                  «       «      }t        |«      xr t        d„ |D «       «      S )z£True when every non-operator token is >=3 chars: a shorter token produces no
        trigrams, and with FTS5's implicit AND one such token empties the whole MATCH.rY   c              3   ó8   K  — | ]  }t        |«      d k\  –— Œ y­w©r`   N)r  rh   s     r&   rA   z>SessionSearchMixin._trigram_eligible_tokens.<locals>.<genexpr>g  s   è ø€ Ò#@°A¤C¨£F¨a¥KÑ#@ùs   ‚)rU   r!   rr   Úall)r®  Útokenss     r&   Ú_trigram_eligible_tokensz+SessionSearchMixin._trigram_eligible_tokensb  s:   € ô & e§k¡k°#Ó&6×&<Ñ&<Ó&>Ó?ˆÜ�F‹|Ò@¤Ñ#@¸Ô#@Ó @Ð@r6   rO   c                 ó>   ‡ — t        ˆ fd„t        |«      D «       «      S )u    True when any non-operator CJK token has fewer than 3 CJK chars â€” the trigram
        tokenizer needs >=3 per token, so such a query must take the LIKE route.c              3   ój   •K  — | ]*  }‰j                  |«      sŒ‰j                  |«      d k  –— Œ, y­wrØ  )rº  rË  )r?   rT   r£   s     €r&   rA   z:SessionSearchMixin._has_short_cjk_token.<locals>.<genexpr>m  s.   øè ø€ Òj¨QÐUX×UfÑUfÐghÕUi�3—>‘> !Ó$ qÕ(Ñjùs   ƒ3š3)rL   rU   )r£   rO   s   ` r&   Ú_has_short_cjk_tokenz'SessionSearchMixin._has_short_cjk_tokeni  s   ø€ ô ÓjÔ2FÀyÓ2QÔjÓjÐjr6   c                 ón   — | j                  |«      dk\  xr  | j                  |«       xr | j                  S )u”   Per-token CJK length gate for the trigram index: ``å¹¿è¥¿ OR æ¡‚æž— OR æ¼“æ±Ÿ`` has 6
        CJK chars total but 2 per token, so trigram returns 0.r`   )rË  rÞ  rÍ   )r®   rO   s     r&   Ú_trigram_route_okz$SessionSearchMixin._trigram_route_oko  s>   € ð —‘ 	Ó*¨aÑ/ò ,¸×8QÑ8QÐR[Ó8\Ð4\ò ,Ø×+Ñ+ð	-r6   c                 ó4  — 	 | j                   ry| j                  |xs d«      }|sy| j                  |«      sy|j                  d«      j                  «       }| j                  r| j                  |«      sy| j                  |«      rdS dS # t        $ r Y y	w xY w)
z>Best-effort name of the routing path a query takes (log-only).Úlike_scan_fts_staler   ÚemptyÚfts5rY   Úfts_cjkÚtrigramÚ	like_scanr¤   )Ú
_fts_stalerÀ  rº  r!   rÚ   rÎ  rà  r«   )r®   r®  r½  Úraws       r&   Ú_describe_search_pathz(SessionSearchMixin._describe_search_pathu  s–   € ð	Ø�ŠØ,Ø×1Ñ1°%²+¸2Ó>ˆIÙØØ×%Ñ% iÔ0ØØ—/‘/ #Ó&×,Ñ,Ó.ˆCØ×&Ò&¨t×/EÑ/EÀcÔ/JØ Ø $× 6Ñ 6°sÔ ;�9ÐLÀÐLøÜò 	Ùð	ús,   ‚B �B §B ¹<B Á6B Â	B Â	BÂBrÖ   Úmatch_queryÚorder_by_sqlÚoffsetc                óŽ   — | › d�g}|g}t        ||fi |¤Ž |j                  ||g«       t        d| › d�| › d| › d�||d«      }||fS )zHMATCH query + params against one FTS5 index joined to messages/sessions.z MATCH ?zsnippet(z), -1, '>>>', '<<<', '...', 40) AS snippetz'
            JOIN messages m ON m.id = z.rowidúLIMIT ? OFFSET ?)r‹   rŠ   r}   )	rÖ   rë  rì  r   rí  Úfiltersrw   r€   râ   s	            r&   Ú_fts_match_sqlz!SessionSearchMixin._fts_match_sqlˆ  su   € ð �7˜(Ð#Ð$ˆØ#�}ˆÜ˜u fÑ8°Ò8Ø�‰�u˜f�oÔ&Ü Ø�u�gÐFÐGØˆgÐ=¸e¸WÀFÐKÈUÐT`Ðbtó
ˆð �Fˆ{Ðr6   ©Ú	fail_openÚoperational_debugró  rô  c                ó€  —  | j                   |||fi |¤Ž\  }}	 | j                  ||«      D �	cg c]  }	t        |	«      ‘Œ c}	S c c}	w # t        j                  $ r |rt
        j                  |d¬«       Y yt        j                  $ r5}
|�| j                  |
«      s‚ t
        j                  d||
«       Y d}
~
yd}
~
ww xY w)u­  Run one MATCH against *table*; ``None`` when the query cannot execute (tokenizer /
        syntax) so the caller falls back. *fail_open* names the index for the
        substring-capable routes: a corruption-class ``DatabaseError`` there detaches the
        derived indexes (``_enter_fts_fail_open``) and answers from canonical rows â€” a live
        search never runs the unbounded rebuild. Other ``DatabaseError``s propagate.T)Úexc_infoNz[%s FTS search hit a corruption error (%s); detached FTS and falling back to canonical LIKE.)
rñ  rÃ   rk   rü   rý   r¬   rþ   ÚDatabaseErrorÚ_enter_fts_fail_openr­   )r®   rÖ   rë  rì  ró  rô  Úkwargsrâ   r€   rø   r¯   s              r&   Ú_match_rowszSessionSearchMixin._match_rows–  sµ   € ð *�d×)Ñ)¨%°¸lÑUÈfÑU‰ˆˆVð	Ø)-¯©¸¸VÓ)DÖE #”D˜•IÒEÐEùÒEøÜ×'Ñ'ò 	Ù Ü—‘Ð.¸�Ô>ÙÜ×$Ñ$ò 	ØÐ ¨×(AÑ(AÀ#Ô(FØÜ�N‰NØmØ˜3ô ô ûð	ús.   šA ®AÁ A ÁA Á,B=Á6B=Â+B8Â8B=rw   r€   rx   ry   c                ó„   — t        t        d|||«      }| j                  ||«      D �cg c]  }t        |«      ‘Œ c}S c c}w )zDCanonical-table LIKE scan; ``params[0]`` is the snippet anchor term.z
messages m)r}   Ú_LIKE_SNIPPET_SQLrÃ   rk   )r®   rw   r€   rx   ry   râ   rø   s          r&   Ú
_like_rowszSessionSearchMixin._like_rows¬  s9   € ä Ô!2°LÀ%ÈÐS\Ó]ˆØ%)§^¡^°C¸Ó%@ÖA˜c”�S•	ÒAÐAùÒAs   ¨=c                 ó˜  — g g}d}t         j                  | «      D ]†  }|j                  «       }|dk(  r|d   r|j                  g «       d}Œ1|dv rŒ6|dk(  rd}Œ>|j	                  d«      j	                  d«      j	                  «       }|sŒo|d   j                  ||f«       d}Œˆ g }g }d	}|D ]‰  }	|	rt        d
„ |	D «       «      sŒg }
|	D ]G  \  }}|
j                  |r	dt        › �nt        «       |j                  t        |«      «       |�ŒC|rŒF|}ŒI |j                  ddj                  |
«      › d�«       Œ‹ dj                  |«      ||fS )zªCompile the supported FTS boolean subset into LIKE predicates: terms within an OR
        group are ANDed (FTS5's implicit conjunction) and ``NOT`` negates the next term.Fr*   r  >   r+   r-   r,   TrY   r¶  Nc              3   ó(   K  — | ]
  \  }}| –— Œ y ­wrI   r=   )r?   r@   Únegateds      r&   rA   zASessionSearchMixin._compile_like_boolean_query.<locals>.<genexpr>Ì  s   è ø€ Ò#H±J°A°w¨¤KÑ#Hùs   ‚zNOT r8  r{   rC   rÐ  )
rÑ  rÒ  rR   r‰   r!   rL   Ú_LIKE_COALESCED_COLUMN_SQLrŠ   rb   rD   )r®  ÚgroupsÚnegate_nextrÔ  Úoperatorr]   Úcompiled_groupsr€   Úsnippet_termr³  Úclausesr   s               r&   Ú_compile_like_boolean_queryz.SessionSearchMixin._compile_like_boolean_query±  su  € ð 13¨tˆØˆÜ'×/Ñ/°Ó6ò 	$ˆIØ —‘Ó(ˆHØ˜4ÒØ˜"’:Ø—M‘M "Ô%Ø#�ØØ˜?Ñ*ØØ˜5Ò Ø"�ØØ—?‘? 3Ó'×-Ñ-¨cÓ2×8Ñ8Ó:ˆDÚØ�r‘
×!Ñ! 4¨Ð"5Ô6Ø#‘ð	$ð" &(ˆØˆØ&*ˆØò 		AˆEÙ¤Ñ#HÀ%Ô#HÔ HØØ!#ˆGØ!&ò (‘��gØ—‘Ág Ô&@Ð%AÑBÔSmÔnØ—‘œl¨4Ó0Ô1ØÑ'²Ø#'‘Lð	(ð
 ×"Ñ" Q w§|¡|°GÓ'<Ð&=¸QÐ#?Õ@ð		Að �{‰{˜?Ó+¨V°\ÐAÐAr6   Úsortc                ó  — | j                  |«      \  }}}|r|€g S d|› d�g}	t        |	|fi |¤Ž t        |t        «      r#|j	                  «       j                  «       dk(  rdnd}
| j                  |	|g|¢|‘|‘d|
› d|
› �d¬	«      S )
z;Search canonical messages while derived FTS state is stale.r8  rC   r/   rŽ  r�  zORDER BY m.timestamp z, m.id rï  ©rx   ry   )r  r‹   ri   r    r!   Úlowerrý  )r®   r®  r   rí  r	  rð  Ú	predicater€   r  rw   rˆ  s              r&   Ú_search_messages_like_fallbackz1SessionSearchMixin._search_messages_like_fallback×  s°   € ð +/×*JÑ*JÈ5Ó*QÑ'ˆ	�6˜<Ù˜LÐ0ØˆIØ�Y�K˜qÐ!Ð"ˆÜ˜u fÑ8°Ò8Ü# D¬#Ô.°4·:±:³<×3EÑ3EÓ3GÈ8Ò3S‘ÐY_ˆØ�‰˜u |Ð&L°fÐ&L¸eÐ&LÀVÐ&LØ*?À¸wÀgÈeÈWÐ(UÐasð ó uð 	ur6   c                 óÖ   — | j                   s| j                  sy	 | j                  dt        f«      }|�d| _         dx| _        x| _        | _        yy# t        j
                  $ r Y yw xY w)z@Observe fail-open initiated by another process sharing state.db.Nz.SELECT 1 FROM state_meta WHERE key = ? LIMIT 1TF)rè  r¨   Ú	_read_oner   rü   ÚErrorrÍ   rÚ   )r®   Ústales     r&   Ú_refresh_fts_stale_statez+SessionSearchMixin._refresh_fts_stale_stateã  sr   € à�?Š? $×"3Ò"3Øð	Ø—N‘NÐ#SÔVcÐUeÓfˆEð ÐØ"ˆDŒOØTYÐYˆDÔÐY Ô 7¸$Õ:Qð øô �}‰}ò 	Ùð	ús   ›A ÁA(Á'A(ÚmatchesÚresult_fieldsc                 óR  — |�d|v �rt        dt        |«      d«      D ]ý  }|||dz    }|D �ci c]  }|d   g “Œ
 }}	 t        j                  dj	                  d„ |D «       «      ¬«      }| j                  «       5 }|j                  |t        |«      «      j                  «       }	ddd«       	D ]  }
||
d	      j                  |
«       Œ 	 |D ]M  }	 |j                  |d   g «      D �
cg c](  }
|
d
   t        | j                  |
d   «      «      dd dœ‘Œ* c}
|d<   ŒO Œÿ |D ]  }|j                  dd«       Œ |�(|D ��cg c]  }|D �ci c]  }||v sŒ|||   “Œ c}‘Œ }}}|S c c}w # 1 sw Y   ŒÆxY w# t        $ r i }Y Œ·w xY wc c}
w # t        $ r g |d<   Y ŒÎw xY wc c}w c c}}w )zOAttach neighboring messages in bounded batches, only when context is requested.Nr—   r   iô  rŽ   r9   c              3   ó    K  — | ]  }d –— Œ y­wr;   r=   r>   s     r&   rA   z>SessionSearchMixin._finalize_search_matches.<locals>.<genexpr>÷  s   è ø€ ÒAXÈ!Ä#ÑAXùrB   )ÚidsÚmatch_idr�   r£  éÈ   )r�   r£  )Úranger  Ú_CONTEXT_WINDOW_SQLrÙ   rD   r  r2   rj   r  r‰   r«   rl   ro   r¨  Úpop)r®   r  r  ÚstartÚbatchÚmatchÚcontextsrâ   r4   rÅ   rø   rœ   s               r&   Ú_finalize_search_matchesz+SessionSearchMixin._finalize_search_matchesï  sò  € ð Ð  I°Ò$>Ü˜q¤# g£,°Ó4ò .�Ø  e¨c¡kÐ2�Ø9>Ö?°˜E $™K¨™OÐ?�Ð?ð"Ü-×4Ñ4¸¿¹ÑAXÈxÔAXÓ9XÐ4ÓY�CØŸ™Ó)ð L¨TØ#Ÿ|™|¨C´°h³Ó@×IÑIÓK˜÷Là#ò >˜Ø   Z¡Ñ1×8Ñ8¸Õ=ñ>ð #ò .�Eð.ð (0§|¡|°E¸$±KÀÓ'Dö,Fà #ð &)¨¡[¼]È4×K_ÑK_Ð`cÐdmÑ`nÓKoÓ=pÐquÐruÐ=vÓwò,F˜˜iÒ(ñ.ð.ð& ò 	'ˆEØ�I‰I�i Õ&ð	'àÐ$Øgn×oÐ^c¸ÖY°È%ÐSXÊ.˜˜u U™|Ñ+ÔYÐoˆGÑoØˆùò+ @÷Lð Lûô !ò "Ø!’Hð"üò,Føô %ò .Ø+-˜˜iÓ(ð.üò ZùÓosp   ­E#½<E4Á9*E(Â#&E4ÃF
Ã'-FÄF
Ä>	F#Å	FÅFÅF#Å(E1	Å-E4Å4FÆFÆF
Æ
FÆFÆF#r‚   rƒ   r„   r~   r   c                 óö  — t        j                   «       }d}	 | j                  |||||||||	|
|¬«      }|t        j                   «       |z
  dz  }|t        «       k\  r9t        j	                  d| j                  |«      ||�t        |«      nd|dd «       S S # t        j                   «       |z
  dz  }|t        «       k\  r9t        j	                  d| j                  |«      ||�t        |«      nd|dd «       w w xY w)z§:meth:`_search_messages_impl` plus one log line per slow search with the routing
        path taken. Threshold HERMES_SEARCH_SLOW_MS (default 1000; 0 logs every call).N)
r‚   rƒ   r„   r   rí  r	  r�   r˜   r~   r   r   z<slow session search: path=%s elapsed=%.0fms rows=%s query=%rÚerrr  )ri  Ú_search_messages_implr'   r¬   rÐ   rê  r  )r®   r®  r‚   rƒ   r„   r   rí  r	  r�   r˜   r~   r   ÚstartedrÅ   Ú
elapsed_mss                  r&   Úsearch_messagesz"SessionSearchMixin.search_messages  s  € ô —)‘)“+ˆØˆð	*Ø×-Ñ-Ø ]ÀOÐalØ F°ÐHXÐagØ!¨Yð .ó 8ˆDð äŸ)™)›+¨Ñ/°6Ñ9ˆJØœ_Ó.Ò.Ü—‘ÐZØ ×6Ñ6°uÓ=¸zÐX\ÐXhÌ3ÈtÌ9ÐnsØ! % C˜Lõ*ð /øô Ÿ)™)›+¨Ñ/°6Ñ9ˆJØœ_Ó.Ò.Ü—‘ÐZØ ×6Ñ6°uÓ=¸zÐX\ÐXhÌ3ÈtÌ9ÐnsØ! % C˜Lõ*ð /ús   ˜B ÂA"C8c                 ób  ‡— | j                  |	«      }|r|j                  «       sg S | j                  |«      }|sg S t        |||||
|¬«      }|r.d|v r* | j                  |f|||dœ|¤Ž}| j                  ||¬«      S | j                  «        | j                  r* | j                  |f|||dœ|¤Ž}| j                  ||¬«      S | j                  sg S t        j                  t        |t        «      r|j                  «       j                  «       ndd«      }t        d|||dœ|¤Ž}t        |«      xr d|v xs t        |«      xr t        d„ |D «       «      }| j!                  |«      }|r| j#                  |||«      }n@ | j$                  d	|fi |¤Ž\  }}	 | j'                  ||«      D �cg c]  }t        |«      ‘Œ }}| j1                  «       �\t3        |«      |k  rN	  | j4                  ||t3        |«      z
  fi |¤Ž}|D �ch c]  }|d
   ’Œ	 c}Š|j7                  ˆfd„|D «       «       |s”|s’t        |«      rd|v sƒt=        |j                  d«      j                  «       «      }| j>                  r | j@                  d|fi |¤Žxs |}|s5| jB                  r)| jE                  |«      r | j@                  d|fi |¤Žxs |}|s<|s:| j                  s.| jG                  |«      }|� | j@                  d	|fdddœ|¤Žxs |}| j                  ||¬«      S c c}w # t(        j*                  $ r g cY S t(        j,                  $ r4}| j/                  |«      s‚  | j                  |f|||dœ|¤Ž}Y d}~�Œ°d}~ww xY wc c}w # t(        j*                  $ r!}t8        j;                  d|«       Y d}~�Œ€d}~ww xY w)ab  FTS5 search across session messages (keywords, ``"phrases"``, AND/OR/NOT, ``prefix*``).
        Returns snippet + session metadata + 1-message context per hit; ``fields`` selects a
        projection. ``sort``: None = BM25 rank; "newest"/"oldest" = timestamp then rank (the
        CJK LIKE fallback ignores it). Rewound rows (``active=0, compacted=0``) are excluded
        by default; compaction-archived rows ARE included; ``include_inactive`` = every row.
        ``after_ts``/``before_ts`` bound ``sessions.started_at`` on every route (FTS5, CJK,
        trigram, LIKE fallback, unindexed-gap supplement).©r�   r‚   rƒ   r„   r~   r   Útool)r   rí  r	  )r  NzORDER BY rank)rì  r   rí  c              3   ó,   K  — | ]  }|t         v –— Œ y ­wrI   )r   )r?   Úsrcs     r&   rA   z;SessionSearchMixin._search_messages_impl.<locals>.<genexpr>L  s   è ø€ Ò'eÐPS¨Ô/KÔ(KÑ'eùs   ‚rè   rŽ   c              3   ó2   •K  — | ]  }|d    ‰vsŒ|–— Œ y­w)rŽ   Nr=   )r?   r�  Úseen_idss     €r&   rA   z;SessionSearchMixin._search_messages_impl.<locals>.<genexpr>h  s   øè ø€ ÒQ Q¸¸4¹ÈÒ9PœqÑQùs   ƒ�z$Unindexed-gap supplement skipped: %srY   rÔ   r6  z
OR-relaxedz1OR-relaxed FTS retry failed; keeping empty resultrò  r=   )$r¥   r!   rÀ  rk   r  r"  r  rè  r¨   Ú_FTS_ORDER_BYrl   ri   r    r  rr   rL   rº  Ú_search_cjkrñ  rÃ   rü   rý   r÷  rø  r¶   r  Ú_search_unindexed_gaprŠ   r¬   rþ   r\   rÚ   rú  rÍ   rÛ  rÕ  )r®   r®  r‚   rƒ   r„   r   rí  r	  r�   r˜   r~   r   r  rð  r  rì  ÚrouteÚwants_unindexed_rowsÚis_cjkrâ   r€   rø   r¯   Úgap_matchesr�  Úfb_queryÚrelaxedr/  s                              @r&   r%  z(SessionSearchMixin._search_messages_impl%  sã  ø€ ð ×3Ñ3°FÓ;ˆÙ˜EŸK™KœMØˆIØ×)Ñ)¨%Ó0ˆÙØˆIÜÐ(8ÈØ'6ÀKØ (°Iô?ˆñ
 ˜6 [Ñ0Ø9�d×9Ñ9¸%ÐrÀuÐU[ÐbfÑrÐjqÑrˆGØ×0Ñ0°ÈÐ0ÓVÐVØ×%Ñ%Ô'Ø�?Š?Ø9�d×9Ñ9¸%ÐrÀuÐU[ÐbfÑrÐjqÑrˆGØ×0Ñ0°ÈÐ0ÓVÐVØ× Ò ØˆIä$×(Ñ(ÄÈDÔRUÔAV¨¯©«×);Ñ);Ô)=Ð\`ÐbqÓrˆÜÐV ,°eÀFÑVÈgÑVˆô !% [Ó 1Ò K°fÀÐ6Kò  gÜ�ÓÒe¤CÑ'eÐWdÔ'eÓ$eð 	à×#Ñ# EÓ*ˆÙØ×&Ñ& uÐ.BÀEÓJ‰Gà-˜$×-Ñ-¨n¸eÑMÀuÑM‰KˆC�ðwØ04·±¸sÀFÓ0KÖL¨œ4 �9ÐL�ÐLð" ×"Ñ"Ó$Ð0´S¸³\ÀEÒ5IðJØ8˜d×8Ñ8¸ÀÌÈGËÑ@TÑ`ÐX_Ñ`�Ø-4Ö5¨˜A˜d›GÒ5�Ø—‘ÓQ¨+ÔQÔQñ ™v¬t°KÔ/@ÀVÈ{ÑEZÜ(¨¯©°SÓ)9×)?Ñ)?Ó)AÓBˆHð ×&Ò&Ø*˜$×*Ñ*Ð+=¸xÑQÈ5ÑQÒ\ÐU\�Ù˜t×6Ò6¸4×;XÑ;XÐY^Ô;_Ø*˜$×*Ñ*Ð+AÀ8ÑUÈuÑUÒ`ÐY`�ñ ™v¨d¯oªoØ×,Ñ,¨UÓ3ˆGØÐ"Ø*˜$×*Ñ*¨>¸7ð 4ÈlØ=pñ4à-2ñ4ò ?à7>ð ð ×,Ñ,¨WÀMÐ,ÓRÐRùòu MøÜ×+Ñ+ò Ø’	Ü×(Ñ(ò 	wð ×0Ñ0°Ô5ØØ=˜$×=Ñ=¸eÐvÈ5ÐY_ÐfjÑvÐnuÑv–ûð	wüò" 6øä×+Ñ+ò JÜ—‘ÐCÀS×IÒIûðJúsZ   ÆL Æ)LÆ;L Ç$M: È M5ÈM: ÌL ÌM2Ì,M2Ì>)M-Í-M2Í5M: Í:N.ÎN)Î)N.r4  r3  c                 ót  — |j                  d«      j                  «       }t        |«      }| j                  r.|s,| j                  |«      s | j                  d|fdddœ|¤Ž}|�|S | j                  |«      r|s | j                  d|fddi|¤Ž}|�|S t        |«      xs |g}|D ��	cg c]  }t        |«      D ]  }	|	‘Œ Œ }
}}	d	d
j                  t        gt        |«      z  «      › d�g}dD �ci c]  }|||   “Œ
 }}t        ||
fi |¤Ž | j                  ||d   g|
¢|d   ‘|d   ‘dd¬«      S c c}	}w c c}w )u÷  CJK routing: the unicode61 table splits CJK into single characters (false positives,
        missed phrases). cjk-bigram serves every shape except queries wanting rows the
        substring indexes exclude (role='tool', cron/subagent sources) and LONE
        1-char CJK runs (bigrams only exist for runs >=2 â€” LIKE is broader); then trigram
        (>=3 CJK chars per token); then a LIKE substring scan with one clause per
        non-operator token so "å¹¿è¥¿ OR æ¡‚æž— OR æ¼“æ±Ÿ" matches each term.rY   rÔ   z
CJK-bigramz;messages_fts_cjk query failed; falling back to trigram/LIKErò  r6  ró  ÚTrigramr8  rÐ  rC   r*  r   r   rí  úORDER BY m.timestamp DESCrï  r  )r!   r\   rÚ   rÎ  rú  rà  rU   rb   rD   Ú_LIKE_ANY_COLUMN_SQLr  r‹   rý  )r®   r®  r4  r3  rO   rë  r  Únon_op_tokensr[   rm   Úlike_paramsÚ
like_whereÚkrð  s                 r&   r1  zSessionSearchMixin._search_cjk�  sœ  € ð —K‘K Ó$×*Ñ*Ó,ˆ	Ü'¨	Ó2ˆØ×"Ò"Ñ+?È×H^ÑH^Ð_hÔHiØ&�d×&Ñ&Ø" KðjØ;GØ"_ñjàchñjˆGð Ð"Ø�Ø×!Ñ! )Ô,Ñ5IØ&�d×&Ñ&Ð'=¸{ÑiÐV_ÐiÐchÑiˆGØÐ"Ø�Ü,¨YÓ7ÒF¸I¸;ˆØ*7×S 3ÄÈcÓARÒS¸AšQÐS˜QÐSˆÑSØ˜&Ÿ+™+Ô';Ð&<¼sÀ=Ó?QÑ&QÓRÐSÐSTÐUÐVˆ
ð)Bö C 1�1�e˜A‘h‘;ð Cˆð Cä˜z¨;ÑB¸'ÒBà�‰˜z¨M¸!Ñ,<Ð+l¸{Ð+lÈEÐRYÉNÐ+lÐ\aÐbjÑ\kÐ+lØ(CÐOað ó cð 	cùó TùòCs   Â'D/Ã.D5Ú	fts_queryc                 ón  — | j                  «       }|�|dk  rg S d„ t        j                  |«      D «       D �cg c]  }|r|j                  «       t        vr|‘Œ }}|sg S dgt
        gt        |«      z  ¢}|d   |d   gd„ |D «       ¢}t        ||fi |¤Ž | j                  ||d   g|¢|‘dd¬	«      S c c}w )
uö   LIKE-scan ids in (fts_rebuild_progress, fts_rebuild_high_water] â€” rows the deferred
        rebuild hasn't indexed yet. The FTS query degrades to AND-joined substring terms
        (quoted phrases kept whole): recall-over-precision mid-rebuild.r   c              3   óx   K  — | ]2  }|j                  d «      j                  d«      j                  «       –— Œ4 y­w)rY   r¶  N)r!   rh   s     r&   rA   z;SessionSearchMixin._search_unindexed_gap.<locals>.<genexpr>³  s,   è ø€ Ò lÀQ §¡¨£×!3Ñ!3°CÓ!8×!>Ñ!>×!@Ñ lùs   ‚8:zm.id > ? AND m.id <= ?rÁ   rÀ   c              3   ó@   K  — | ]  }t        |«      D ]  }|–— Œ Œ y ­wrI   )rb   )r?   r]   rm   s      r&   rA   z;SessionSearchMixin._search_unindexed_gap.<locals>.<genexpr>¸  s$   è ø€ Ò=nÀDÔ[gÐhlÓ[mÒ=nÐVW¼aÐ=n¸aÑ=nùs   ‚r;  zLIMIT ?r  )	r¶   rÑ  rÒ  rR   Ú_LIKE_SKIP_TOKENSr<  r  r‹   rý  )	r®   rA  r   rð  r  r[   Útermsrw   r€   s	            r&   r2  z(SessionSearchMixin._search_unindexed_gap¬  sé   € ð ×(Ñ(Ó*ˆØˆ>˜U ašZØˆIÙ lÌ.×J`ÑJ`ÐajÓJkÔ lö B˜Ù˜CŸI™I›KÔ/@Ñ@ò ð Bˆð BáØˆIØ)ÐRÔ.BÐ-CÄcÈ%ÃjÑ-PÐRˆØ˜yÑ)¨6°'©?ÐoÑ=nÈEÔ=nÐoˆÜ˜u fÑ8°Ò8Ø�‰˜u u¨Q¡xÐ&@°&Ð&@¸%Ð&@ÐKfØ)2ð ó 4ð 	4ùòBs   ¸!B2Úinclude_archivedr”   Úsourcesc           
      óT  ‡‡— |xs dj                  «       j                  «       Š‰r|dk  rg S | j                  |||t        |dz  |«      d|d‰¬«      }dt        t
        t        f   dt        fˆfd„Št        t        |«      ˆfd	„¬
«      }|d| D �	�
cg c]  \  }	}
|
‘Œ	 c}
}	S c c}
}	w )z©Search surfaced sessions by exact/prefix/substring session id. Also matches
        ``_lineage_root_id`` so an old compression root id resolves to the live continuation.r   r   é   T)r”   rH  rƒ   r   rí  rG  Úorder_by_last_activeÚid_queryrø   r   c                 ó  •— t        | j                  d«      xs d«      t        | j                  d«      xs d«      fD �cg c]  }|sŒ|j                  «       ‘Œ }}t        ˆfd„|D «       «      ryt        ˆfd„|D «       «      rdS dS c c}w )	NrŽ   r   Ú_lineage_root_idc              3   ó(   •K  — | ]	  }|‰k(  –— Œ y ­wrI   r=   ©r?   r%   Úneedles     €r&   rA   zJSessionSearchMixin.search_sessions_by_id.<locals>.score.<locals>.<genexpr>Í  s   øè ø€ Ò; u�5˜F•?Ñ;ùs   ƒr   c              3   ó@   •K  — | ]  }|j                  ‰«      –— Œ y ­wrI   )Ú
startswithrP  s     €r&   rA   zJSessionSearchMixin.search_sessions_by_id.<locals>.score.<locals>.<genexpr>Ï  s   øè ø€ ÒM¸˜E×,Ñ,¨V×4ÑMùs   ƒr  r  )r    rl   r  rL   )rø   ÚvÚ
normalizedrQ  s      €r&   Úscorez7SessionSearchMixin.search_sessions_by_id.<locals>.scoreË  s{   ø€ Ü.1°#·'±'¸$³-Ò2EÀ2Ó.FÌÈCÏGÉGÐTfÓLgÒLmÐkmÓHnÐ-oÖu¨Òst˜!Ÿ'™'�)ÐuˆJÐuÜÓ;°
Ô;Ô;ØÜÓMÀ*ÔMÔM�1ÐTÐSTÐTùò vs   Á B	ÁB	c                 ó$   •—  ‰| d   «      | d   fS )Nr  r   r=   )ÚitemrV  s    €r&   ú<lambda>z:SessionSearchMixin.search_sessions_by_id.<locals>.<lambda>Ð  s   ø€ ÁÀtÈAÁwÃÐQUÐVWÑQXÐ@Y€ r6   )r0   N)
r!   r  Úlist_sessions_richrl  r   r    r   rs   r¡   r¼  )r®   r®  r   rG  r”   rH  rƒ   Ú
candidatesÚrankedr@   rø   rQ  rV  s              @@r&   Úsearch_sessions_by_idz(SessionSearchMixin.search_sessions_by_id½  sº   ù€ ð
 ’+˜2×$Ñ$Ó&×,Ñ,Ó.ˆÙ˜ !šØˆIð ×,Ñ,Ø 7¸OÔSVÐW\Ð_`ÑW`ÐbgÓShØÐ'7ÈdÐ]cð -ó eˆ
ð	U”tœC¤˜H‘~ð 	U¬#õ 	Uô
 œ	 *Ó-Ó3YÔZˆØ"(¨¨% .×1™˜˜3’Ó1Ð1ùÓ1s   ÂB$rp   c                 óv   — 	 | j                   j                  d|› d�«       y# t        j                  $ r Y yw xY w)u    True if an FTS5 virtual table is queryable ("no such table" and "vtable
        constructor failed" â€” missing tokenizer / mid-teardown â€” both count as not).zSELECT 1 FROM z LIMIT 0TF)r)  r2   rü   r÷  )r®   rp   s     r&   Ú_fts_table_existsz$SessionSearchMixin._fts_table_existsÕ  s=   € ð	Ø�J‰J×Ñ °¨v°XÐ>Ô?ØøÜ×$Ñ$ò 	Ùð	ús   ‚" ¢8·8c                 ód   — | j                   D �cg c]  }| j                  |«      sŒ|‘Œ c}S c c}w )z3Queryable FTS tables (caller holds ``self._lock``).)Ú_FTS_TABLESr_  )r®   r  s     r&   Ú_present_fts_tablesz&SessionSearchMixin._present_fts_tablesÞ  s)   € à#×/Ñ/ÖO˜°4×3IÑ3IÈ#Õ3N’ÒOÐOùÒOs   �-¦-c           	      ó´  — | j                  «        d}| j                  5  | j                  «        | j                  €| j	                  d¬«       | j                  «       D ]*  }	 | j                  j                  d|› d|› d�«       |dz  }Œ, 	 ddd«       |S # t        j                  $ r!}t        j                  d	||«       Y d}~Œgd}~ww xY w# 1 sw Y   |S xY w)
a»  Merge fragmented FTS5 segments into one per index (``'optimize'``). Pure
        maintenance: changes neither results nor ``snippet()`` output, only layout and
        speed; VACUUM then returns the freed pages. Returns the number optimized. A quarantined
        handle never issues ``'optimize'``: it rewrites index segments in place and would compound
        structural damage (or a split WAL generation) instead of leaving it diagnosable.r   NÚwrite©r—   r7  r8  z) VALUES('optimize')r  zFTS optimize failed for %s: %s)Ú_raise_if_db_corruptr'  Ú_raise_if_db_replacedr)  Ú_reopen_after_close_lockedrb  r2   rü   rý   r¬   r­   )r®   Ú	optimizedr  r¯   s       r&   Úoptimize_ftszSessionSearchMixin.optimize_ftsâ  sß   € ð 	×!Ñ!Ô#Øˆ	Ø�Z‰Zñ 		OØ×&Ñ&Ô(Ø�z‰zÐ!Ø×/Ñ/¸Ð/Ô@Ø×/Ñ/Ó1ò O�ðOØ—J‘J×&Ñ&¨°c°U¸!¸C¸5Ð@TÐ'UÔVØ ‘N‘IñO÷			Oð Ðøô ×/Ñ/ò OÜ—N‘NÐ#CÀSÈ#×NÑNûðOú÷		Oð Ðús6   ŸACÁ"'BÂ	CÂC
Â)CÃ CÃC
Ã
CÃCc           
      ó²  — | j                  «        d}t        | j                  «      5 }|st        j	                  d«       	 ddd«       y| j
                  5  | j                  «        | j                  €| j                  d¬«       | j                  «       D ]D  }	 | j                  j                  d|› d|› d�«       | j                  j                  «        |d	z  }ŒF 	 ddd«       ddd«       |S # t        j                  $ r;}| j                  j                  «        t        j	                  d
||«       Y d}~Œ£d}~ww xY w# 1 sw Y   ŒdxY w# 1 sw Y   |S xY w)u©  Rebuild FTS5 indexes from ``messages`` (``'rebuild'``) â€” the recovery for a corrupt index
        that rejects writes while reads succeed. Two processes rebuilding one state.db
        concurrently corrupted production DBs, so this admits through
        ``fts_rebuild_admission`` and FAILS CLOSED, returning 0 on deferral (callers treat 0
        as "no progress" and use the stale-FTS breadcrumb path). Returns indexes rebuilt.

        Uses the FTS5 ``'rebuild'`` command, which rewrites the internal b-tree segments from the content
        rows. Unlike ``optimize_fts`` (which merges existing segments), ``rebuild`` discards and recreates
        the index data entirely â€” the more destructive of the two, so it is quarantined the same way. See
        #50502.
        A full structural rebuild must never run concurrently in two processes sharing one state.db â€” that
        interleaving has structurally corrupted the database in production (PR #93200) â€” so this admits
        through the cross-process ``fts_rebuild_admission`` authority and FAILS CLOSED: if another process
        holds the rebuild lock beyond the bounded wait, this call defers (returns 0) rather than racing it.
        Callers already treat 0 as "rebuild made no progress" and fall back to the stale-FTS breadcrumb
        path, which retries in-process from the gateway housekeeping tick (``retry_deferred_fts_recovery``)
        and at next startup.
        r   z]Deferred in-place FTS rebuild: another process holds the rebuild authority for this state.db.Nrd  re  r7  r8  z) VALUES('rebuild')r  zFTS rebuild failed for %s: %s)rf  r   Údb_pathr¬   r­   r'  rg  r)  rh  rb  r2   r*  rü   rý   Úrollback)r®   ÚrebuiltÚadmittedr  r¯   s        r&   Úrebuild_ftszSessionSearchMixin.rebuild_ftsö  sH  € ð& 	×!Ñ!Ô#ØˆÜ" 4§<¡<Ó0ð 	R°HÙÜ—‘Øsôuà÷		Rð 	Rð
 —‘ñ RØ×*Ñ*Ô,Ø—:‘:Ð%Ø×3Ñ3¸GÐ3ÔDØ×3Ñ3Ó5ò R�CðRØŸ
™
×*Ñ*¨\¸#¸¸aÀ¸uÐDWÐ+XÔYØŸ
™
×)Ñ)Ô+Ø 1™™ñ	R÷	R÷	Rð" ˆøô #×3Ñ3ò RØŸ
™
×+Ñ+Ô-ÜŸ™Ð'FÈÈS×QÑQûðRú÷Rð Rú÷	Rð" ˆúsU   ¨EÁ
EÁAE ÂAC/ÃE ÃEÃ/D=	Ä1D8	Ä3E Ä8D=	Ä=E Å E		ÅEÅE)Úmax_commandsr§   rq  c          
      ó  — t        d|«       |€| j                  }t        d|«       d}| j                  5  | j                  «       D ]�  }| j                  s"| j
                  j                  d|› d|› d�«       t        |«      D ]_  }| j
                  j                  }| j
                  j                  d|› d|› d�|f«       |d	z  }| j
                  j                  |z
  d
k  sŒ_ Œ� ŒŸ d| _        ddd«       |S # 1 sw Y   |S xY w)a^  Run bounded FTS5 ``'merge'`` commands against each present index. A positive merge rank
        stops after ~that many output pages, so each command holds the write lock for
        milliseconds regardless of index size (``'optimize'`` takes 9-18 s per index on a
        10 GB DB). ``usermerge`` is lowered to its minimum of 2 (persisted in ``%_config``,
        once per instance) so a merge acts on ANY level with >= 2 segments; at the default
        4 a fragmented index cannot converge. Up to *max_commands* per index, stopping on
        the no-progress signal ``total_changes`` delta < 2 (the INSERT itself is 1). Each
        command is its own implicit transaction, so processes interleave mid-pass. Missing
        tables are skipped (optimize_fts_storage drops + backfills them live); other SQLite
        errors propagate. Returns commands executed.r§   Nrq  r   r7  r8  z, rank) VALUES('usermerge', 2)z, rank) VALUES('merge', ?)r  r  T)	rt   Ú_FTS_MERGE_COMMANDS_PER_PASSr'  rb  Ú_fts_usermerge_floor_appliedr)  r2   r  Útotal_changes)r®   r§   rq  Úexecutedr  r@   Úbefores          r&   r©   z+SessionSearchMixin._merge_fts_incrementally  s  € ô 	�k 9Ô-ØÐØ×<Ñ<ˆLÜ�n lÔ3ØˆØ�Z‰Zñ 
	5Ø×/Ñ/Ó1ò �Ø×8Ò8Ø—J‘J×&Ñ&¨°c°U¸!¸C¸5Ð@^Ð'_Ô`Ü˜|Ó,ò �AØ!ŸZ™Z×5Ñ5�FØ—J‘J×&Ñ&¨°c°U¸!¸C¸5Ð@ZÐ'[Ð^gÐ]iÔjØ ‘M�HØ—z‘z×/Ñ/°&Ñ8¸1Ó<Ùñðð 15ˆDÔ-÷
	5ð ˆ÷
	5ð ˆús   µB+C7Ã!C7Ã7D)r   N)r  r`   )ÚuserÚ	assistant)é   FrI   )
NNNrz  r   NFNNN)rz  TNNN)OÚ__name__Ú
__module__Ú__qualname__Ú__doc__r    Úclassmethodr   r   r    r	   r¥   r°   r   r   r¶   r¹   r´   rØ   rÌ   r   rÎ   r   rì   rí   rÒ   rÜ   r   rr   rÏ   rð   rô   rî   r  r%  r#  r-  r3  Ústaticmethodr1  r2  rs   r@  rE  rJ  rP  rO  rY  r]  r   r{  r’  r­  rÀ  rº  rË  rÎ  rÕ  rÛ  rÞ  rà  rê  rj   rñ  rú  rý  r  r  r  r"  r(  r%  r1  r2  r]  r_  rb  rj  rp  r©   r=   r6   r&   r�   r�   ¨   sÏ  „ ÙHð%Ð!ð ð
a¨H°ZÀ±_Ñ,Eð 
aÈ(ÐSXÐY\Ð^aÐYaÑSbÑJcò 
aó ð
aóQð"3 H¨T°#°s°(©^Ñ$<ó 3ð
7¨°°c¸3°h±Ñ(@ó 7ðx cð x¨h°t¸CÀ¸H±~Ñ.Fó xð$	Mð ð	Rð ð	AáAXÐY\ÓA]Ð@^ð _Zð	Zð  ð	1à1NÐ0Oð P`ð	`ð ð	AáAXÐY\ÓA]Ð@^ð	`ð ó	OóOð! cð !°t¸EÀ#ÀtÀ)Ñ<LÑ7Mð !ÐRVó !ð
[ $ó 
[ð\ dó \ð 16ò' Cð '°d¸3±ið 'Ècð 'Ø)-ð'Ø:>ó'ðR>¨$ó >ó@0ó& ð
	¸tó 	ó
Dð ð¨ò ó ðð òOó ðOð @Eò ¸ð Èó ó!ð&¨ó ð((¨Só (ðT	 °cð 	 ¸dó 	 ð $ó ð0¨°©ó ð$ LPÐ`dòM3Ø& x°°c¸3°h±Ð0@À$Ð0FÑ'GÑHðM3ØY]ðM3à	ˆc�3ˆh‰óM3ðd XYØ0Eñ,
Øð,
Ø25ð,
Ø?Bð,
ØQTð,
à˜U 3¨ 8™_Ñ-ð,
àJNÈsÐTWÈxÉ.ó,
ð^ JOñ#Øð#Ø&)ð#ØBFð#ØSWÐX\Ð]`ÐbeÐ]eÑXfÑSgó#ðN ð"! Cð "!¨Cò "!ó ð"!ðH ð4˜Cð 4 Dò 4ó ð4ð ð9˜ð 9 ò 9ó ð9ð ð ð ¨ò ó ðð ð? ð ?¨°#©ò ?ó ð?ð ðA¨ð A°ò Aó ðAð ðk¨Sð k°Tò kó ðkð
-¨3ð -°4ó -ð¨3ð °3ó ð& ð˜cð °ð À3ð ÐRUð Ð_bð Ø%*¨3°¨9Ñ%5òó ðð jnØ7;ò ð °3ð Àcð ÐYaÐbeÑYfð Ø'/°¡}ðØJRÐSWÐX\Ð]`ÐbeÐ]eÑXfÑSgÑJhóð,B  S¡	ð B°4ð BÀcð BÐVYð BÐ^bÐcgÐhkÐmpÐhpÑcqÑ^ró Bð
 ð#B¨3ð #B°5¸¸dÀ3¹iÈÐRUÉÐ9VÑ3Wò #Bó ð#BðJ
uØð
uØ$'ð
uØ14ð
uØ<DÀS¹Mð
uØY]Ð^bÐcfÐhkÐckÑ^lÑYmó
uó
Zð Y]ñØ˜D  c ™NÑ+ðØ<DÀZÐPSÁ_Ñ<UðØaeÐfjÐknÐpsÐksÑftÑauóð@ Y]ØUYØLPØCGñ	*Øð*Ø)-¨c©ð*ØLPÐQTÉIð*à˜#‘Yð*à.1ð*à@Cð*àORð*ð ð*ð 19¸ÀC¹Ñ0Ið*ð ˜3‘-ð	*ð 4<¸C±=ð	*ð
 
ˆd�3˜�8‰nÑ	ó*ð0 Y]ØUYØLPØCGñ	hSØðhSØ)-¨c©ðhSØLPÐQTÉIðhSà˜#‘YðhSà.1ðhSà@CðhSàORðhSð ðhSð 19¸ÀC¹Ñ0IðhSð ˜3‘-ð	hSð 4<¸C±=ð	hSð
 
ˆd�3˜�8‰nÑ	óhSðTc ð c¸Dð cÈÈcÐSVÈhÉð cÐ\`ÐaeÐfiÐknÐfnÑaoÑ\pó cð:4¨sð 4¸3ð 4ÈdÐSWÐX[Ð]`ÐX`ÑSaÑNbó 4ð$ Y]Ø@Dñ2Øð2Ø!$ð2Ø=Að2ØRUð2à�c‘ð2à48¸±Ið2àIMÈdÐSVÐX[ÐS[ÉnÑI]ó2ð0 cð ¨dó ðP T¨#¡Yó Pð˜có ð(&˜Só &ðP Y]ò °Sð ÈÐQTÉð Ðadô r6   r�   )Fr~  r9  Úloggingr»  rü   ri  Útypingr   r   r   r   r   r   r	   Úagent.skill_commandsr
   Úhermes_state_commonr   r   r   r   r   r   r   r   r   r   r   ra   r   r   r   Ú	getLoggerr¬   r"   r'   Ú_FTS5_SPECIAL_CHARSÚcompileÚescaper¹  Ú	frozensetrS   rE  rÑ  r·  r|   rü  r<  r  r0  r  rM   r    ÚRowr5   rE   rs   rr   rN   rU   r\   rb   ro   rt   r}   rj   r‹   r�   ÚjsonÚosr=   r6   r&   ú<module>r�     sŠ  ðñó Û Û 	Û Û ß I× IÑ Iå :÷÷ ÷ ÷ ð 
ˆ×	Ñ	˜>Ó	*€ð˜ó ð 5Ð Ø�2—:‘:  ) "§)¡)Ð,?Ó"@Ð!AÀÐCÓDÐ áÒ/Ó0€Ø" f XÑ-Ð Ø�—‘˜NÓ+€Ø�B—J‘J˜zÓ*Ð ð eÐ ØYÐ àið ð5ð ð =ÐHhÑi€ð$Ð ð€ðY˜ð Y ¨'¯+©+Ñ!6ó Yð
_˜cð _ dó _ð9�ð 9˜ó 9ðM Cð M¨D°©Ió Mð ð ¨ó ð+�sð +˜t C™yó +ð
7˜3ð 7 3ó 7ð>˜ð > Cð >¨Dó >ð Cð °3ð ¸tÀC¹yð ÐTWð Ðdgð Ðloó ð" #¸tò&Ø�‰9ð&Ø"ð&Ø9=ð&ØNVÐW[Ð\_ÑW`ÑNað&à˜d 3™iÑ(ð&à7?ÀÀSÁ	Ñ7Jð&ð �s‰mð&ð 08¸©}ð&ð IMó&÷<Qñ Qóp$ Ü 	r6   