Ë
    âß²j€) ã            	       óü  — U d Z ddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlmZmZmZmZ ddlmZ ddlmZ ddlmZ ddlmZmZmZmZmZmZmZmZmZm Z m!Z!m"Z"m#Z#m$Z$m%Z%m&Z&m'Z' ddl(m)Z) dd	l*m+Z+  ejX                  d
«      Z-dZ.dZ/dZ0dZ1dZ2dZ3de4de5fd„Z6da7ee8   e9d<    e8d„ e$D «       «      Z: e8d„ e$D «       «      Z;eefee fdœZ<dZ=dZ>dZ?dj�                  e?jƒ                  «       d   ge?jƒ                  «       dd D � cg c]  } d| j…                  «       z   ‘Œ c} z   dgz   «      ZCdZDd ZEd!ZFd"ZGd#ZHd$ZId%d&œd'e5d(e4d)eJde5fd*„ZKd+e5de5fd,„ZLde8fd-„ZM G d.„ d/«      ZNd0ejž                  ddfd1„ZPyc c} w )2zôSchema creation, column reconciliation, and FTS DDL management 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)ÚDictÚListÚOptionalÚSequence©Úget_hermes_home)Úreport_startup_progress)Úsafe_json_loads)ÚDEFERRED_INDEX_SQLÚFTS_CJK_STALE_KEYÚFTS_REBUILD_DEFERRAL_KEYÚFTS_STALE_KEYÚFTS_SQLÚFTS_STORAGE_VERSIONÚFTS_TOOL_CONTENT_PREFIX_CHARSÚFTS_TRIGRAM_SQLÚLEGACY_FTS_SQLÚLEGACY_FTS_TRIGRAM_SQLÚ
SCHEMA_SQLÚSCHEMA_VERSIONÚ_FTS_CJK_TRIGGERSÚ_FTS_TRIGGERSÚ_ephemeral_child_sqlÚ_sql_json_extractÚfts_rebuild_admission)Ú_drop_orphan_fts_shadow_tables)Ú_read_proc_argvÚhermes_stateé   ç      N@é
   g      œ@g      ¬@ÚpidÚreturnc                 óH   — t        | «      }|rdj                  |«      d d S dS )NÚ éx   z<cmdline unavailable>)r   Újoin)r"   Úargvs     ú8/home/ubuntu/.hermes/hermes-agent/hermes_state_schema.pyÚ_holder_cmdliner*   /   s)   € Ü˜3Ó€DÙ#'ˆ3�8‰8�D‹>˜$˜3ÐÐDÐ-DÐDó    Ú_READ_PROBE_STATEMENTSc              #   ó*   K  — | ]  }d |v sŒ|–— Œ y­w)Ú	_trigram_N© ©Ú.0Úns     r)   ú	<genexpr>r3   9   s   è ø€ ÒK A¸+ÈÒ:JœaÑKùs   ‚	Œc              #   ó2   K  — | ]  }|t         vsŒ|–— Œ y ­w©N)Ú_FTS_TRIGRAM_TRIGGERSr0   s     r)   r3   r3   :   s   è ø€ ÒV °qÔ@UÒ7Uœ1ÑVùs   ‚�)FTz[COALESCE(content, '') || ' ' || COALESCE(tool_name, '') || ' ' || COALESCE(tool_calls, '') )z]CREATE INDEX IF NOT EXISTS idx_session_model_usage_session ON session_model_usage(session_id)zVCREATE INDEX IF NOT EXISTS idx_session_model_usage_model ON session_model_usage(model)a_  CREATE TABLE session_model_usage (
    session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
    model TEXT NOT NULL,
    billing_provider TEXT NOT NULL DEFAULT '',
    billing_base_url TEXT NOT NULL DEFAULT '',
    billing_mode TEXT NOT NULL DEFAULT '',
    task TEXT NOT NULL DEFAULT '',
    api_call_count INTEGER NOT NULL DEFAULT 0,
    input_tokens INTEGER NOT NULL DEFAULT 0,
    output_tokens INTEGER NOT NULL DEFAULT 0,
    cache_read_tokens INTEGER NOT NULL DEFAULT 0,
    cache_write_tokens INTEGER NOT NULL DEFAULT 0,
    reasoning_tokens INTEGER NOT NULL DEFAULT 0,
    estimated_cost_usd REAL NOT NULL DEFAULT 0,
    actual_cost_usd REAL NOT NULL DEFAULT 0,
    cost_status TEXT,
    cost_source TEXT,
    first_seen REAL,
    last_seen REAL,
    PRIMARY KEY (session_id, model, billing_provider, billing_base_url, billing_mode, task)
)Ú
é   éÿÿÿÿz#                                   z                                )a  INSERT OR IGNORE INTO session_model_usage (
                               session_id, model, billing_provider,
                               billing_base_url, billing_mode,
                               api_call_count, input_tokens,
                               output_tokens, cache_read_tokens,
                               cache_write_tokens, reasoning_tokens,
                               estimated_cost_usd, actual_cost_usd,
                               cost_status, cost_source, first_seen, last_seen
                           )
                           SELECT id, COALESCE(model, 'unknown'),
                                  COALESCE(billing_provider, ''),
                                  COALESCE(billing_base_url, ''),
                                  COALESCE(billing_mode, ''),
                                  COALESCE(api_call_count, 0),
                                  COALESCE(input_tokens, 0),
                                  COALESCE(output_tokens, 0),
                                  COALESCE(cache_read_tokens, 0),
                                  COALESCE(cache_write_tokens, 0),
                                  COALESCE(reasoning_tokens, 0),
                                  COALESCE(estimated_cost_usd, 0),
                                  COALESCE(actual_cost_usd, 0),
                                  cost_status, cost_source,
                                  started_at, COALESCE(ended_at, started_at)
                           FROM sessions
                           WHERE COALESCE(input_tokens, 0)
                                 + COALESCE(output_tokens, 0)
                                 + COALESCE(cache_read_tokens, 0)
                                 + COALESCE(cache_write_tokens, 0)
                                 + COALESCE(reasoning_tokens, 0) > 0zfCREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_title_unique ON sessions(title) WHERE title IS NOT NULLziINSERT INTO state_meta (key, value) VALUES (?, '1') ON CONFLICT(key) DO UPDATE SET value = excluded.valueúgINSERT INTO state_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.valuezVDELETE FROM state_meta WHERE key IN ('fts_rebuild_high_water', 'fts_rebuild_progress')zEDELETE FROM state_meta WHERE key = 'fts_tool_full_content_high_water'F©Údelete_firstÚtableÚindentr<   c                ób   — d|z  }|r|› d| › d�nd}d|› |› d| › d|› d|› d	|› d
|› d|› d|dd › �S )zNLegacy inline (pre-v23) FTS re-population script fragment (whitespace pinned).r%   úDELETE FROM z;
Ú r7   úINSERT INTO z(rowid, content)
zSELECT id,
z'       COALESCE(content, '') || ' ' ||
z)       COALESCE(tool_name, '') || ' ' ||
z        COALESCE(tool_calls, '')
zFROM messages;
Néüÿÿÿr/   )r=   r>   r<   ÚpadÚbodys        r)   Ú_legacy_inline_reinsert_sqlrF   �   sp   € à
�‰,€CÙ-9ˆcˆU�,˜u˜g SÑ)¸r€Dà
ˆTˆF�3�%�| E 7Ð*<Øˆ%ˆ|Øˆ%Ð7Øˆ%Ð9Øˆ%Ð0Øˆ%Ð  C R ˜zð	+ðr+   Úidentc                 ó2   — d| j                  dd«      z   dz   S )zDouble-quote an SQL identifier.ú"z"")Úreplace)rG   s    r)   Ú_qrK   ›   s   € à�—‘˜s DÓ)Ñ)¨CÑ/Ð/r+   c                  óž   — t         €Bt        j                  t        «      } t	        d„ t        | j                  «       «      D «       «      a t         S )a	  SELECT statements that fail iff a live store is behind SCHEMA_SQL. Read-only opens skip
    ``_reconcile_columns()`` (no DDL against another profile's live DB), so healing callers
    run these afterwards: a missing table/column raises at prepare time. Derived from
    SCHEMA_SQL (a hand-maintained list went stale within days). Columns are
    table-qualified: an unqualified double-quoted identifier that fails to resolve silently
    degrades to a string literal (SQLite misfeature) and would pass on the stale store.c              3   óŠ   ‡K  — | ]:  \  Š}d j                  dj                  ˆfd„|D «       «      t        ‰«      «      –— Œ< y­w)zSELECT {} FROM {} LIMIT 0ú, c              3   óP   •K  — | ]  }t        ‰«      › d t        |«      › �–— Œ y­w)ú.N)rK   )r1   Úcolr=   s     €r)   r3   z9schema_read_probe_statements.<locals>.<genexpr>.<genexpr>«   s%   øè ø€ Ò8bÐVY¼B¸u»I¸;ÀaÌÈ3ËÀyÔ9QÑ8bùs   ƒ#&N)Úformatr'   rK   )r1   Úcolsr=   s     @r)   r3   z/schema_read_probe_statements.<locals>.<genexpr>ª   sA   øè ø€ ò '
á��tð (×.Ñ.¨t¯y©yÓ8bÐ]aÔ8bÓ/bÔdfÐglÓdm×nñ'
ùs   ƒA A)r,   ÚSessionSchemaMixinÚ_parse_schema_columnsr   ÚtupleÚsortedÚitems)Útabless    r)   Úschema_read_probe_statementsrZ       sE   € ô Ð%Ü#×9Ñ9¼*ÓEˆÜ!&ñ '
ä% f§l¡l£nÓ5ô'
ó "
Ðô "Ð!r+   c                   ó6  — e Zd ZdZdej
                  ddfd„Zdej
                  defd„Zdej
                  ddfd„Z	e
dej
                  dee   defd	„«       Ze
d
ee   defd„«       Zdej
                  defd„Ze
dej
                  defd„«       Ze
dej
                  deddfd„«       Ze
dej
                  deddfd„«       Zdej
                  deddfd„Ze
dej
                  dedefd„«       Zdej
                  defd„Zdej
                  ddfd„Ze
dddœdej
                  dededdfd„«       Zdej
                  dedee   fd„Zdej
                  defd„Zddœdej
                  dedefd „Zdefd!„Zdej
                  dedefd"„Zdej
                  defd#„Ze
d$ede ee eef   f   fd%„«       Z!dej
                  ddfd&„Z"e
dej
                  d'edee#e      fd(„«       Z$e
d8dej
                  d'ed)eded*eddfd+„«       Z%e
d9d,„«       Z&dej
                  ddfd-„Z'dej
                  ddfd.„Z(d/„ Z)dej
                  d0ed1eddfd2„Z*dej
                  ddfd3„Z+dej
                  ddfd4„Z,dej
                  ddfd5„Z-d9d6„Z.dej
                  ddfd7„Z/y):rT   u>   See module docstring â€” mixin for SessionDB (Schema cluster).Úcursorr#   Nc                 óF  — 	 |j                  d«      j                  «       }|D ],  \  }}	 | j	                  ||«      }|j                  d||f«       Œ. y# t        j                  $ r Y yw xY w# t        j                  $ r!}t
        j                  d|«       Y d}~ yd}~ww xY w)aU  Move inline prompt snapshots into the shared content-addressed table. Any
        ``OperationalError`` mid-loop returns instead of raising: partial migration is safe
        (the legacy column stays a read fallback; next init resumes), whereas propagating
        left the version below 25 and re-ran this on every open (gateway crash loop).zFSELECT id, system_prompt FROM sessions WHERE system_prompt IS NOT NULLNzMUPDATE sessions SET system_prompt_hash = ?, system_prompt = NULL WHERE id = ?z�v25 prompt dedupe paused after contention (%s); unmigrated rows keep the legacy inline prompt and the next schema init resumes the migration.)ÚexecuteÚfetchallÚsqlite3ÚOperationalErrorÚ_store_system_promptÚloggerÚwarning)Úselfr\   ÚrowsÚ
session_idÚpromptÚprompt_hashÚexcs          r)   Ú_dedupe_legacy_system_promptsz0SessionSchemaMixin._dedupe_legacy_system_prompts´   s®   € ð
	Ø—>‘>Ð"jÓk×tÑtÓvˆDð #'ò 	ÑˆJ˜ðØ"×7Ñ7¸ÀÓG�Ø—‘ØcØ  *Ð-õñ	øô ×'Ñ'ò 	Ùð	ûô ×+Ñ+ò Ü—‘ðtàôõ
 ûðús(   ‚A ª&A,ÁA)Á(A)Á,B Á?BÂB c                 óÐ   — 	 |j                  d«       |j                  d«       y# t        j                  $ r-}| j                  |«      s‚ | j	                  |«       Y d }~yd }~ww xY w)Nz:CREATE VIRTUAL TABLE temp._hermes_fts5_probe USING fts5(x)z"DROP TABLE temp._hermes_fts5_probeTF)r^   r`   ra   Ú_is_fts5_unavailable_errorÚ_warn_fts5_unavailable)re   r\   rj   s      r)   Ú_sqlite_supports_fts5z(SessionSchemaMixin._sqlite_supports_fts5Ì   s\   € ð	Ø�N‰NÐWÔXØ�N‰NÐ?Ô@ØøÜ×'Ñ'ò 	Ø×2Ñ2°3Ô7ØØ×'Ñ'¨Ô,Üûð		ús   ‚"% ¥A%¸#A Á A%c                 óÔ   — | j                  |«       t        D ]B  }t        j                  t        j
                  «      5  |j                  d|› �«       d d d «       ŒD y # 1 sw Y   ŒOxY w)NúDROP TRIGGER IF EXISTS )Ú_drop_fts_triggersr   Ú
contextlibÚsuppressr`   ra   r^   )re   r\   Útriggers      r)   Ú_drop_all_fts_triggersz)SessionSchemaMixin._drop_all_fts_triggers×   sh   € Ø×Ñ Ô'Ü(ò 	DˆGÜ×$Ñ$¤W×%=Ñ%=Ó>ñ DØ—‘Ð!8¸¸	ÐBÔC÷Dð Dñ	D÷Dð Dús   ¾AÁA'	Únamesc                 óÆ   — |sydj                  d„ |D «       «      }d|› d�}t        | j                  |t        |«      «      j	                  «       d   «      t        |«      k  S )z;True unless every trigger in *names* (one DDL half) exists.Fú,c              3   ó    K  — | ]  }d –— Œ y­w©ú?Nr/   ©r1   Ú_s     r)   r3   z;SessionSchemaMixin._fts_triggers_missing.<locals>.<genexpr>â   s   è ø€ Ò3¨¤Ñ3ùó   ‚zGSELECT COUNT(*) FROM sqlite_master WHERE type = 'trigger' AND name IN (ú)r   )r'   Úintr^   rV   ÚfetchoneÚlen)r\   rw   ÚplaceholdersÚsqls       r)   Ú_fts_triggers_missingz(SessionSchemaMixin._fts_triggers_missingÝ   s`   € ñ ØØ—x‘xÑ3¨UÔ3Ó3ˆØWÐXdÐWeÐefÐgˆÜ�6—>‘> #¤u¨U£|Ó4×=Ñ=Ó?ÀÑBÓCÄcÈ%ÃjÑPÐPr+   r…   c                 óv   — | sydj                  | j                  «       «      j                  «       }d|vxr d|v S )z?True when trigger SQL is a broad AFTER UPDATE (missing ``OF``).Fr%   zAFTER UPDATE OF zAFTER UPDATE ON )r'   ÚsplitÚupper)r…   Úcompacts     r)   Ú#_fts_update_trigger_needs_narrowingz6SessionSchemaMixin._fts_update_trigger_needs_narrowingæ   s=   € ñ ØØ—(‘(˜3Ÿ9™9›;Ó'×-Ñ-Ó/ˆØ!¨Ð0ÒRÐ5GÈ7Ð5RÐRr+   c                 óJ  — | j                  |«      }d|rdndz   }dj                  d„ |D «       «      }d|› d�}|j                  ||«      j                  «       }|D ��cg c]  \  }}| j	                  |«      sŒ|‘Œ }}}|sy|D ]  }|j                  d	|› �«       Œ t
        |   \  }	}
| j                  |d
|	«       | j                  |d|
«       d|v rn	 | j                  |«       |j                  dd«      j                  «       }|r| j	                  |d   «      r&| j                  |«       t        j                  d«       t        j                  dt        |«      «       t        |«      S c c}}w # t        $ r( | j                  |«       t        j                  d«       ‚ w xY w)a  Replace broad AFTER UPDATE FTS triggers with AFTER UPDATE OF variants (``IF NOT EXISTS``
        never replaces an existing broad trigger). No FTS rebuild: correctness was already
        gated by WHEN clauses; OF only skips trigger evaluation. Returns the number dropped.)Úmessages_fts_updateÚmessages_fts_trigram_updater/   )Úmessages_fts_cjk_updaterN   c              3   ó    K  — | ]  }d –— Œ y­wr{   r/   r}   s     r)   r3   zHSessionSchemaMixin._migrate_broad_fts_update_triggers.<locals>.<genexpr>ø   s   è ø€ Ò ;¨¤Ñ ;ùr   zHSELECT name, sql FROM sqlite_master WHERE type = 'trigger' AND name IN (r€   r   rq   Úmessages_ftsÚmessages_fts_trigramr�   z2CJK FTS re-ensure after UPDATE OF migration failedzASELECT sql FROM sqlite_master WHERE type = 'trigger' AND name = ?zeCJK FTS UPDATE trigger missing or still broad after UPDATE OF migration; marked stale and unavailablezPMigrated %d broad FTS UPDATE trigger(s) to AFTER UPDATE OF (no rebuild required))Ú_db_has_legacy_inline_ftsr'   r^   r_   r‹   Ú_FTS_DDLÚ_ensure_fts_schemaÚ_ensure_fts_cjk_schemaÚ	ExceptionÚ)_quarantine_cjk_after_update_of_migrationrc   Ú	exceptionr‚   rd   Úinforƒ   )re   r\   Úlegacy_layoutÚupdate_namesr„   r…   rf   ÚnameÚto_dropÚbase_sqlÚtrigram_sqlÚrows               r)   Ú"_migrate_broad_fts_update_triggersz5SessionSchemaMixin._migrate_broad_fts_update_triggersî   s¶  € ð ×6Ñ6°vÓ>ˆØMÙ‰BÐ%Añ
ˆð —y‘yÑ ;¨lÔ ;Ó;ˆØXÐYeÐXfÐfgÐhˆØ�~‰~˜c <Ó0×9Ñ9Ó;ˆØ)-×_™I˜D #°×1YÑ1YÐZ]Õ1^’4Ð_ˆÑ_ÙØØò 	=ˆDØ�N‰NÐ4°T°FÐ;Õ<ð	=ô !)¨Ñ 7Ñˆ�+Ø×Ñ ¨¸ÔAØ×Ñ Ð(>ÀÔLð %¨Ñ/ðØ×+Ñ+¨FÔ3ð
 —.‘.ØSÐUqóç‰h‹jð ñ ˜$×BÑBÀ3ÀqÁ6ÔJØ×>Ñ>¸vÔFÜ—‘ðHôô 	�‰ÐfÔhkÐlsÓhtÔuÜ�7‹|Ðùó; `øô ò Ø×>Ñ>¸vÔFÜ× Ñ Ð!UÔVØðús   ÁE+Á8E+ÃE1 Å11F"c                 ób   — | j                  d«      j                  «       }|duxr d|d   xs dvS )u¬  True when ``messages_fts`` is still external-content over the raw
        ``messages`` table (FTS_STORAGE_VERSION < 3): its index holds a TRUNCATED
        projection for long tool rows that the checker/'delete' commands re-read
        as FULL content, a mismatch by construction. Such an index cannot be
        repaired in place â€” it must be 'rebuild'-filled from the aligned
        ``messages_fts_src`` view exactly once.zLSELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'messages_fts'NÚmessages_fts_srcr   rA   ©r^   r‚   )r\   r¡   s     r)   Ú_fts_index_is_misaligned_sourcez2SessionSchemaMixin._fts_index_is_misaligned_source  s>   € ð �n‰nØZó
ç
‰(‹*ð 	ð ˜$ˆÒKÐ#5¸cÀ!¹fºlÈÐ#KÐKr+   Úddlc                 ó~  — t        | j                  d«      j                  «       «      }d}d}|j                  «       D ]Ù  }||dz   z  }t	        j
                  |«      sŒ!d|j                  «       }}|j                  «       }|j                  d«      r|}ŒW|j                  d«      rL|j                  ddd«      j                  d	«      }|j                  |j                  dd
«      d   «      |k(  rd}Œ´|�| j                  |«       d}| j                  |«       ŒÛ |j                  «       rt	        j                  d«      ‚y)u‹  Run *ddl* statement by statement, skipping each ``DROP TRIGGER IF EXISTS x`` /
        ``CREATE TRIGGER IF NOT EXISTS x â€¦`` pair whose trigger already exists with the same body.

        ``DROP TRIGGER IF EXISTS`` on an existing trigger takes SQLite's write lock (the
        ``CREATE â€¦ IF NOT EXISTS`` forms do not), so the unconditional drop+recreate that lets a
        changed trigger body roll out would otherwise block every open of a settled database
        behind a sibling process's transaction. ``sqlite_master.sql`` stores the CREATE text
        verbatim minus ``IF NOT EXISTS`` and the ``;``, so an exact comparison decides.
        z:SELECT name, sql FROM sqlite_master WHERE type = 'trigger'NrA   r7   rq   zCREATE TRIGGER IF NOT EXISTS zIF NOT EXISTS r8   ú;r   é   zincomplete DDL statement)Údictr^   r_   Ú
splitlinesr`   Úcomplete_statementÚstripr‰   Ú
startswithrJ   ÚrstripÚgetrˆ   ra   )	r\   r§   ÚstoredÚpending_dropÚ	statementÚlineÚcurrentr‰   Údesireds	            r)   Ú&_execute_ddl_skipping_settled_triggersz9SessionSchemaMixin._execute_ddl_skipping_settled_triggers'  s.  € ô �f—n‘nÐ%aÓb×kÑkÓmÓnˆØ&*ˆØˆ	Ø—N‘NÓ$ò 	$ˆDØ˜ ™Ñ$ˆIÜ×-Ñ-¨iÔ8ØØ!# Y§_¡_Ó%6�wˆIØ—M‘M“OˆEØ×ÑÐ 9Ô:Ø&�ØØ×ÑÐ ?Ô@Ø!Ÿ/™/Ð*:¸BÀÓB×IÑIÈ#ÓN�Ø—:‘:˜gŸm™m¨D°!Ó4°QÑ7Ó8¸GÒCØ#'�LØØÐ'Ø—‘˜|Ô,Ø#�Ø�N‰N˜7Õ#ð#	$ð$ �?‰?ÔÜ×*Ñ*Ð+EÓFÐFð r+   c                 óÞ   — d}|j                  «       D ]3  }||dz   z  }t        j                  |«      sŒ!| j                  |«       d}Œ5 |j	                  «       rt        j
                  d«      ‚y)zAExecute a DDL script without ``executescript``'s implicit commit.rA   r7   zincomplete FTS DDL statementN)r¬   r`   r­   r^   r®   ra   )r\   r§   r´   rµ   s       r)   Ú!_execute_ddl_script_transactionalz4SessionSchemaMixin._execute_ddl_script_transactionalJ  so   € ð ˆ	Ø—N‘NÓ$ò 	ˆDØ˜ ™Ñ$ˆIÜ×)Ñ)¨)Õ4Ø—‘˜yÔ)Ø‘	ð		ð
 �?‰?ÔÜ×*Ñ*Ð+IÓJÐJð r+   Úlegacyc                óˆ  ‡ ‡‡— |s‰ j                  ‰d«      sy‰ j                  ‰«      sy‰j                  d«      j                  «       duŠdˆˆˆ fd„}‰s+‰j                  d«       	  |«        ‰j                  d«       y‰ j                  ‰|«       y# t        $ r$ ‰j                  d«       ‰j                  d«       ‚ w xY w)	u’  Re-point ``messages_fts`` at the stable ``messages_fts_src`` projection view and
        rebuild it ONCE (FTS_STORAGE_VERSION 2 -> 3). A v1/v2 base index carries token streams
        the raw-``messages`` external-content source cannot read back (truncated long tool
        rows, and tool rows whose full content was indexed under an old high-water mark), so
        in-place continuity is not achievable â€” the ONLY valid transition is a full rebuild
        from the view, under the shared cross-process rebuild admission. Legacy inline DBs
        skip this entirely (their index is self-contained; they still take the DDL on the
        optimize path).r‘   NzSELECT 1 FROM messages LIMIT 1c                  óX  •— t         D ]  } ‰j                  d| › �«       Œ ‰j                  d«       ‰j                  ‰dt        «       ‰r‰j                  d«       ‰j                  t        «       ‰j                  t
        «       ‰j                  t        dt        t        «      f«       y )Nrq   z!DROP TABLE IF EXISTS messages_ftsr‘   z8INSERT INTO messages_fts(messages_fts) VALUES('rebuild')Úfts_storage_version)	Ú_FTS_BASE_TRIGGERSr^   r•   r   Ú_CLEAR_REBUILD_MARKERS_SQLÚ!_DROP_RETIRED_TOOL_HIGH_WATER_SQLÚ_STATE_META_UPSERT_SQLÚstrr   )r�   r\   Úhas_messagesre   s    €€€r)   Údo_alignzCSessionSchemaMixin._migrate_misaligned_fts_source.<locals>.do_aligne  sŒ   ø€ Ü*ò A�Ø—‘Ð!8¸¸Ð?Õ@ðAà�N‰NÐ>Ô?Ø×#Ñ# F¨N¼GÔDÙØ—‘ÐYÔZØ�N‰NÔ5Ô6Ø�N‰NÔ<Ô=Ø�N‰NÔ1Ð4IÌ3ÔObÓKcÐ3dÕer+   zSAVEPOINT fts_align_emptyz!RELEASE SAVEPOINT fts_align_emptyz%ROLLBACK TO SAVEPOINT fts_align_empty©r#   N)Ú_sqlite_table_existsr¦   r^   r‚   ÚBaseExceptionÚ_run_admitted_startup_rebuild)re   r\   r»   rÅ   rÄ   s   ``  @r)   Ú_migrate_misaligned_fts_sourcez1SessionSchemaMixin._migrate_misaligned_fts_sourceV  s»   ú€ ñ ˜×2Ñ2°6¸>ÔJØØ×3Ñ3°FÔ;ØØ—~‘~Ð&FÓG×PÑPÓRÐZ^Ð^ˆ÷		fñ à�N‰NÐ6Ô7ðÙ”
Ø—‘ÐBÔCð
 Ø×*Ñ*¨6°8Õ<øô !ò Ø—‘ÐFÔGØ—‘ÐBÔCØðús   Á(B Â-Cr�   c                 óH   — | j                  d|f«      j                  «       d uS )Nz=SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?r¥   )r\   r�   s     r)   rÇ   z'SessionSchemaMixin._sqlite_table_exists}  s(   € à�~‰~ØKÈdÈWó
ç
‰(‹*˜Dð!ð 	!r+   c                 ó:  ‡— | j                  ‰«      s| j                  ‰«      ry| j                  ‰d«      }|dur|du S t        D ]  }‰j	                  d|› �«       Œ ‰j	                  d«       | j                  ‰dt        «      sy| j                  ‰ˆfd„«       y)a©  Install the source-filtered trigram view and purge historical rows (v29 cron exclusion,
        v30 subagent exclusion: both only change the view/trigger predicate and rebuild from it).
        Legacy inline indexes stay opt-in (their content is private to the vtable). A v1 external
        layout whose vtable still declares ``tool_calls`` is left to ``optimize-storage``: swapping
        the view underneath would make 'rebuild' read a column the view no longer has. Otherwise
        the inverted index still holds excluded rows until FTS5 rebuilds from the new view, which
        runs under the shared cross-process admission gate. Returns False to hold schema_version back.Tr’   Frq   z,DROP VIEW IF EXISTS messages_fts_trigram_srcc                  ó&   •— ‰ j                  d«      S )NzHINSERT INTO messages_fts_trigram(messages_fts_trigram) VALUES('rebuild')©r^   ©r\   s   €r)   ú<lambda>zDSessionSchemaMixin._migrate_trigram_cron_exclusion.<locals>.<lambda>›  s   ø€ �F—N‘NÐ#mÓn€ r+   )r“   Ú%_db_has_trigram_tool_calls_projectionÚ_fts_table_prober6   r^   r•   r   rÉ   )re   r\   Útrigram_existsr�   s    `  r)   Ú_migrate_trigram_cron_exclusionz2SessionSchemaMixin._migrate_trigram_cron_exclusionƒ  s­   ø€ ð ×)Ñ)¨&Ô1°T×5_Ñ5_Ð`fÔ5gØØ×.Ñ.¨vÐ7MÓNˆØ Ñ%ð " UÐ*Ð*Ü)ò 	=ˆDØ�N‰NÐ4°T°FÐ;Õ<ð	=à�‰ÐEÔFØ×&Ñ& vÐ/EÄÔWØð 	×*Ñ*ØÛnô	
ð r+   c                 ó   — d| _         	 | j                  t        d|¬«       	 |j                  d«       y	# t        $ r t        j                  dd¬«       Y Œ5w xY w# t        $ r t        j                  dd¬«       Y y	w xY w)
zÊFail closed after dropping the CJK UPDATE trigger mid-migration: clear availability,
        persist ``fts_cjk_stale``, drop any residual trigger so a later open cannot
        IF-NOT-EXISTS over a gap.FÚ1rÏ   z*Could not persist CJK FTS stale breadcrumbT©Úexc_infoz.DROP TRIGGER IF EXISTS messages_fts_cjk_updatez;Could not drop residual CJK UPDATE trigger after quarantineN)Ú_fts_cjk_availableÚset_metar   r—   rc   Údebugr^   ©re   r\   s     r)   r˜   z<SessionSchemaMixin._quarantine_cjk_after_update_of_migrationŸ  s�   € ð #(ˆÔð	VØ�M‰MÔ+¨S¸ˆMÔ@ð	gØ�N‰NÐKÕLøô ò 	VÜ�L‰LÐEÐPTˆLÖUð	Vûô ò 	gÜ�L‰LÐVÐaeˆLÖfð	gús    ‰4 ¢A ´ AÁAÁ A=Á<A=FT©r»   Úinclude_trigramrÞ   c                óä   — |rdnd}|D ]M  }|r1| j                  d|› �«       | j                  d|› dt        › d�«       Œ6| j                  d|› d|› d�«       ŒO |s| j                  t        «       y	y	)
aR  v23+ external-content 'rebuild'. It indexes EVERY row, so the deferred-backfill
        markers are cleared or the worker would re-insert covered rows (duplicates).
        ``legacy`` (pre-v23 inline layout) has no external-content 'rebuild' source, so it
        DELETEs + reinserts the concatenated content the legacy triggers produced.)r‘   r’   )r‘   r@   rB   z(rowid, content) SELECT id, zFROM messagesú(z) VALUES('rebuild')N)r^   Ú_LEGACY_INLINE_CONCAT_SQLrÀ   )r\   r»   rÞ   rY   Útbls        r)   Ú_rebuild_fts_indexesz'SessionSchemaMixin._rebuild_fts_indexes­  s‰   € ñ >MÑ9ÐRcˆØò 	NˆCÙØ—‘ ¨c¨UÐ3Ô4Ø—‘ ¨c¨UÐ2NÔOhÐNiÐivÐwÕxà—‘ ¨c¨U°!°C°5Ð8KÐLÕMð	Nñ Ø�N‰NÔ5Õ6ð r+   Ú
table_namec                 óÖ  — 	 |j                  d|› d�«       y# t        $ r}|}Y d}~n«d}~wt        j                  $ r’}| j	                  |«      r9| j                  |«      r| j                  |«       n| j                  |«       Y d}~ydt        |«      j                  «       v rY d}~ydt        |«      j                  «       vr‚ |}Y d}~nd}~ww xY wt        j                  d||«       y)	a   True = queryable, False = absent, None = FTS module/tokenizer missing or content
        undecodable (index degraded, store accessible). Invalid UTF-8 surfaces as a bare
        UnicodeDecodeError on some builds and OperationalError("Could not decode to UTF-8")
        on others; both are caught so the probe never raises into init/recovery flows.
        Anything else (malformed schema, corrupt vtable) re-raises.zSELECT * FROM z LIMIT 0TNzno such tableFzdecode to utf-8zp%s probe encountered invalid UTF-8 in FTS content; search may return incomplete results until FTS is rebuilt: %s)r^   ÚUnicodeDecodeErrorr`   ra   rm   Ú_is_trigram_unavailable_errorÚ_warn_trigram_unavailablern   rÃ   Úlowerrc   rd   )re   r\   rä   rj   Ú
decode_excs        r)   rÒ   z#SessionSchemaMixin._fts_table_probe½  sÐ   € ð	Ø�N‰N˜^¨J¨<°xÐ@ÔAØøÜ!ò 	Ø�JûÜ×'Ñ'ò 	Ø×.Ñ.¨sÔ3ð ×5Ñ5°cÔ:Ø×2Ñ2°3Õ7à×/Ñ/°Ô4ÜØ¤# c£(§.¡.Ó"2Ñ2ÜØ ¬¨C«¯©Ó(8Ñ8ØØ�Jûð	úô 	�‰ðLØMWÐYcô	
ð s*   ‚ ˜	C¡(¨C¾ACÂCÂ(CÃCc           	      ó”  — t        j                   «       }	 |j                  dt        f«      j                  «       }|rt        |d   «      nd}t        |t        «      r|ni }	 t        |j                  d|«      «      }t        |j                  dd«      «      dz   }t        |j                  d|«      «      }	t        |j                  dd«      «      dz   }
||kD  s|dk  r|}t        |D ��ch c]  \  }}|dkD  sŒ|’Œ c}}«      }||j                  d	«      k7  r|d}
}	|t        k\  rp||z
  t         k\  rd| j#                  |t         ¬
«      }|rKt$        j'                  d||«       | j)                  «       }t        |D ��ch c]  \  }}|dkD  sŒ|’Œ c}}«      }t+        |«      xr |
t,        k\  xr ||	z
  t.        k\  }|||||	|
|dœ}|j                  dt        t1        j2                  |d¬«      f«       |sd| _        y|| _        |r:t$        j'                  d|
||	z
  dz  dj7                  d„ |D «       «      t8        «       n,|t        k\  r#||z
  t         k\  rt$        j'                  d||«       t$        j;                  d||«       y# t        j
                  $ r d}Y �Œ^w xY w# t        t        f$ r |d|df\  }}}	}
Y �Œæw xY wc c}}w c c}}w )a  Record a deferral diagnostic for the foreign processes holding the DB; True = defer
        (holders remain). After ``_FTS_HOLDER_ESCALATE_ATTEMPTS`` deferrals spanning
        ``_FTS_HOLDER_ESCALATE_SECONDS``, provably inactive orphan Desktop backends are
        reaped and the holders re-checked. The orphan reap is the only exit, so a supervised
        peer (never an orphan) blocks forever: once the SAME PID set has blocked
        ``_FTS_HOLDER_FUTILE_ATTEMPTS`` deferrals over ``_FTS_HOLDER_FUTILE_SECONDS`` the
        record is marked ``futile`` and the escalation names the holders and the remedy that
        works from inside a gateway session (stop only the other holder; this process's own
        retry tick admits the rebuild). A changed holder set restarts that window.z2SELECT value FROM state_meta WHERE key = ? LIMIT 1Nr   Ú
first_seenÚattemptsr8   Úholders_sinceÚholders_attemptsÚholder_pids)Úmin_age_secondszmReaped inactive orphan Desktop backend(s) %s after %d state.db FTS rebuild deferrals; checking holders again.)rì   Ú	last_seenrí   rð   rî   rï   Úfutiler:   T)Ú	sort_keysFu  state.db FTS repair has been blocked by the same holder(s) for %d deferrals over %.0f min (%s); waiting is futile. Stop ONLY the other holder(s) â€” this process keeps running and its own retry admits the rebuild within %.0fs of the holder leaving. `hermes doctor` shows this.r    rN   c              3   ó>   K  — | ]  }d |› dt        |«      › �–— Œ y­w)zpid z: N)r*   )r1   r"   s     r)   r3   zBSessionSchemaMixin._defer_stale_fts_for_holders.<locals>.<genexpr>  s#   è ø€ ÒVÀ3˜D   R¬¸Ó(<Ð'=Ô>ÑVùs   ‚zýstate.db FTS repair remains blocked after %d deferrals by holder(s) %s. Stop the listed processes (this process's own retry then rebuilds), or run `hermes sessions optimize-storage` with every holder stopped. `hermes doctor` reports this degraded state.z¤Deferred stale state.db FTS rebuild while foreign processes hold the database or WAL sidecars (%s); canonical writes and LIKE search remain available (deferral %d).)Útimer^   r   r‚   r`   ÚErrorr
   Ú
isinstancer«   Úfloatr±   r�   Ú	TypeErrorÚ
ValueErrorrW   Ú_FTS_HOLDER_ESCALATE_ATTEMPTSÚ_FTS_HOLDER_ESCALATE_SECONDSÚ%_reap_inactive_orphan_desktop_holdersrc   ÚerrorÚ_foreign_state_db_holdersÚboolÚ_FTS_HOLDER_FUTILE_ATTEMPTSÚ_FTS_HOLDER_FUTILE_SECONDSÚjsonÚdumpsÚ_fts_deferred_holder_pidsr'   Ú_FTS_STALE_RETRY_SECONDSrd   )re   r\   Úforeign_holdersÚnowr¡   ÚparsedÚrecordrì   rí   rî   rï   r"   Ú_pathrð   Úreapedró   Ú
diagnostics                    r)   Ú_defer_stale_fts_for_holdersz/SessionSchemaMixin._defer_stale_fts_for_holdersÞ  sî  € ô �i‰i‹kˆð	Ø—.‘.ØDÔG_ÐFaóç‰h‹jð ñ
 -0”  Q¡Ô(°TˆÜ% f¬dÔ3‘¸ˆð	SÜ˜vŸz™z¨,¸Ó<Ó=ˆJÜ˜6Ÿ:™: j°!Ó4Ó5¸Ñ9ˆHÜ! &§*¡*¨_¸cÓ"BÓCˆMÜ" 6§:¡:Ð.@À!Ó#DÓEÈÑIÐð ˜Ò˜z¨Aš~ØˆJÜ°O×O¡j c¨5ÀsÈQÃwšcÓOÓPˆØ˜&Ÿ*™* ]Ó3Ò3Ø.1°1Ð+ˆMØÔ4Ò4¸¸zÑ9IÔMiÒ9iØ×?Ñ?ØÔ1Mð @ó ˆFñ Ü—‘ðNØOUÐW_ôð #'×"@Ñ"@Ó"B�Ü$¸O×%W©j¨c°5ÈsÐUVËw¢cÓ%WÓX�Ü�kÓ"ò 
ØÔ ;Ñ;ÒqÀÀmÑ@SÔWqÑ@qð 	ð %°3ÀHÐ]hØ*Ð@PÐ\bñ
ˆ
ð 	�‰ØuÜ%¤t§z¡z°*ÈÔ'MÐNô	
ñ Ø-1ˆDÔ*ØØ)4ˆÔ&ÙÜ�L‰Lðoð ! 3¨Ñ#6¸$Ñ">Ø—	‘	ÑVÈ+ÔVÓVÔXpõð Ô6Ò6¸3ÀÑ;KÔOkÒ;kÜ�L‰LðZà[cÐetôô
 	�‰ðwà˜Xô	
ð
 øô{ �}‰}ò 	Ø‹Cð	ûô œ:Ð&ò 	SØDGÈÈCÐQRÀNÑAˆJ˜ -Ó1Að	Süó Pùó &Xs<   –%J Á"A2J Ã*J>
Ã8J>
Å;K
Æ	K
ÊJÊJÊJ;Ê:J;©Útimeout_secondsc                ó  — | j                  «       }|r| j                  ||«      ryt        | j                  |¬«      5 }|st        j                  d«       	 ddd«       y| j                  ||¬«      cddd«       S # 1 sw Y   yxY w)a   Atomically rebuild stale base/trigram indexes and resume syncing. *timeout_seconds*
        bounds the admission wait (None = full startup budget, ``0`` = non-blocking retry).
        Fails closed: holders or a lost admission race leave the breadcrumb set.Fr  z„Deferred stale state.db FTS rebuild: another process holds the rebuild authority; canonical writes and LIKE search remain available.N©r»   )r   r  r   Údb_pathrc   rd   Ú_recover_stale_fts_locked)re   r\   r»   r  r  Úadmitteds         r)   Ú_recover_stale_ftsz%SessionSchemaMixin._recover_stale_fts,  sŒ   € ð ×8Ñ8Ó:ˆÙ˜t×@Ñ@ÀÈÔYØÜ" 4§<¡<ÀÔQð 	IÐU]ÙÜ—‘ðIôð ÷	Ið 	Ið ×1Ñ1°&ÀÐ1ÓH÷	I÷ 	Iò 	Iús   ½A;ÁA;Á;Bc                 óL  — | j                   sy| j                  «       �d| _        d| _        y| j                  s| j
                  €yt        j                  «       }t        | dd«      }|t        | dd«      k  r?|�5t        | j                  «       D ��ch c]  \  }}|dkD  sŒ|’Œ c}}«      |k(  ryd| _        t        t        | dd«      «      }|dk  rt        }||z   | _        t        t        |t        d«      d	z  t        «      | _        	 | j                   5  | j
                  �| j                   s
	 ddd«       y| j
                  j#                  «       }| j%                  |«      }| j'                  ||d¬
«      }|r| j)                  |«       d| _        t+        j,                  t.        j0                  «      5  | j
                  j3                  «        ddd«       |cddd«       S c c}}w # 1 sw Y   ŒxY w# 1 sw Y   yxY w# t4        $ r t6        j9                  dd¬«       Y yw xY w)u  Retry a deferred stale-FTS rebuild (gateway housekeeping tick). ``_recover_stale_fts``
        fails closed at open, leaving search on LIKE; live write/search paths must never
        start a full rebuild, and a gateway opens state.db once for days, so "next open"
        never comes. Bounded doubling backoff, non-blocking admission, no new thread. True
        only when the index was rebuilt and sync triggers restored. Never raises.

        This is the in-process retry: bounded backoff from ``_FTS_STALE_RETRY_SECONDS`` doubling to
        ``_FTS_STALE_RETRY_MAX_SECONDS``, non-blocking admission (``timeout=0``) so a live holder is skipped
        and tried again later, no new thread â€” the caller is an existing periodic tick (gateway
        housekeeping). See #100108, #97940.
        FNg        r  Ú_fts_stale_retry_afterr   Ú_fts_stale_retry_intervalg      ð?g       @)r»   r  zUIn-process retry of the deferred stale state.db FTS rebuild failed; will retry later.Tr×   )Ú
_fts_staleÚ_quarantine_reasonr  r  Ú	read_onlyÚ_connrö   Ú	monotonicÚgetattrrW   r   rù   r  ÚminÚmaxÚ_FTS_STALE_RETRY_MAX_SECONDSÚ_lockr\   r“   r  r–   rs   rt   r`   r÷   Úcommitr—   rc   rd   )	re   r	  Údeferred_pidsr"   r  Úintervalr\   r»   Ú	recovereds	            r)   Úretry_deferred_fts_recoveryz.SessionSchemaMixin.retry_deferred_fts_recovery<  s  € ð �ŠØØ×"Ñ"Ó$Ð0ð +.ˆDÔ'Ø-0ˆDÔ*ØØ�>Š>˜TŸZ™ZÐ/ØÜ�n‰nÓˆÜ Ð&AÀ4ÓHˆØ”˜Ð7¸Ó=Ò=ð Ð$¬Ø'+×'EÑ'EÓ'G×S™˜˜eÈ3ÐQRË7’ÓSó)àò)ð Ø-0ˆDÔ*Üœ Ð'BÀCÓHÓIˆØ�sŠ?Ü/ˆHØ&)¨H¡nˆÔ#Ü),Ü�Ô2°CÓ8¸3Ñ>Ô@\ó*
ˆÔ&ð	Ø—‘ñ !Ø—:‘:Ð%¨T¯_ª_Ø ÷!ð !ð Ÿ™×*Ñ*Ó,�Ø×7Ñ7¸Ó?�Ø ×3Ñ3°FÀ6Ð[^Ð3Ó_�	Ùð ×/Ñ/°Ô7Ø58�DÔ2Ü×(Ñ(¬¯©Ó7ñ (Ø—J‘J×%Ñ%Ô'÷(à ÷!ñ !ùó T÷.(ð (ú÷!ð !ûô ò 	Ü�N‰NØgÐrvð ô ñ ð		úsa   ÂG"
ÂG"
Ä	H  ÄG4Ä/H  Ä8A<G4Æ4G(Ç	G4Ç	H  Ç(G1	Ç-G4Ç4G=Ç9H  Ç=H  È  H#È"H#c                óÜ  — 	 | j                  |d«      du }|xs | xr | j                  |«      }dj                  d„ t        D «       «      }|r|dz  }|dz  }|r4t        |rt        ndz   }|t        dd«      z  }|rE|t        dd	d¬
«      z  }n3t        |rt        ndz   }|dz  }|r|dz  }|t        dz   t        z   dz   z  }d|z   |z   dt        › dt        › d�z   }	 |j!                  |«       d| _        d| _        || _        t0        j;                  d«       y# t        j                  t        f$ r d}Y �Œw xY w# t        j                  $ rž}t#        j$                  t        j&                  «      5  | j(                  j+                  «        ddd«       n# 1 sw Y   nxY w| j-                  |«       | j(                  j/                  «        t0        j3                  d|«       Y d}~yd}~ww xY w)z¨Body of :meth:`_recover_stale_fts`; caller holds rebuild authority. One write
        transaction, so no canonical writer slips between rebuild and trigger restoration.r’   TrA   c              3   ó(   K  — | ]
  }d |› d�–— Œ y­w)rq   r©   Nr/   )r1   ru   s     r)   r3   z?SessionSchemaMixin._recover_stale_fts_locked.<locals>.<genexpr>„  s   è ø€ Ò]ÀGÐ4°W°I¸QÔ?Ñ]ùs   ‚z*DROP TABLE IF EXISTS messages_fts_trigram;zODROP VIEW IF EXISTS messages_fts_trigram_src;DROP TABLE IF EXISTS messages_fts;r‘   é   é   r;   z9INSERT INTO messages_fts(messages_fts) VALUES('rebuild');zIINSERT INTO messages_fts_trigram(messages_fts_trigram) VALUES('rebuild');r©   zBEGIN IMMEDIATE;z&DELETE FROM state_meta WHERE key IN ('z', 'z
');COMMIT;NzfAutomatic rebuild of stale FTS indexes failed (%s); canonical writes remain enabled with FTS detached.FzVRebuilt stale state.db FTS indexes from canonical messages and restored sync triggers.)rÒ   Ú_trigram_tokenizer_availabler`   ÚDatabaseErrorræ   r'   r   r   r   rF   r   r   rÀ   rÁ   r   r   Úexecutescriptrs   rt   r÷   r  Úrollbackrv   r%  rc   rÿ   r  Ú_fts_enabledÚ_trigram_availablerd   )	re   r\   r»   Útrigram_presentrÞ   Údrop_sqlÚrebuild_sqlÚrecovery_sqlrj   s	            r)   r  z,SessionSchemaMixin._recover_stale_fts_lockedy  s  € ð	lØ"×3Ñ3°FÐ<RÓSÐW[Ð[ˆOð
 .Òk°f°*Ò2jÀ×AbÑAbÐciÓAjˆOà—7‘7Ñ]Ì}Ô]Ó]ˆÙØÐDÑDˆHØÐeÑeˆÙÜ(ÁoÕ,BÐ[]Ñ^ˆKØÔ6°~ÀrÓJÑJˆKÙØÔ:Ð;QÐSUÐdhÔiÑi‘ä!¹¥_ÈRÑPˆKØÐVÑVˆKÙØÐjÑj�ØÔ5¸Ñ;Ô>_Ñ_ÐbeÑeÑeˆKà Ñ)¨KÑ7Ø6´}°oÀTÔJbÐIcÐcmÐnñoð 	ð	Ø× Ñ  Ô.ð  ˆŒØ ˆÔØ"1ˆÔÜ�‰ÐoÔpØøôU ×%Ñ%Ô'9Ð:ò 	#à"‹Oð	#ûô6 ×$Ñ$ò 
	Ü×$Ñ$¤W§]¡]Ó3ñ &Ø—
‘
×#Ñ#Ô%÷&÷ &ñ &úð ×'Ñ'¨Ô/Ø�J‰J×ÑÔÜ�L‰LðEØFIôô ûð
	úsB   ‚D ÃD: ÄD7Ä6D7Ä:G+Å#G&Å0FÆ	G&ÆF	ÆAG&Ç&G+c                 óf  — d}|j                  d|› �«       	 |j                  d|› d�«       	 |j                  d|› �«       y# t        j                  $ rA}| j                  |«      s‚ | j	                  |«       Y d}~|j                  d|› �«       yd}~ww xY w# |j                  d|› �«       w xY w)zAProbe trigram support without publishing a persistent FTS object.ztemp.hermes_fts5_trigram_probezDROP TABLE IF EXISTS zCREATE VIRTUAL TABLE z( USING fts5(content, tokenize='trigram')NFT)r^   r`   ra   rç   rè   )re   r\   Úproberj   s       r)   r.  z/SessionSchemaMixin._trigram_tokenizer_availableª  sµ   € à0ˆØ�‰Ð.¨u¨gÐ6Ô7ð	<Ø�N‰NÐ2°5°'Ð9aÐbÕcð �N‰NÐ2°5°'Ð:Ô;Øøô ×'Ñ'ò 	Ø×5Ñ5°cÔ:ØØ×*Ñ*¨3Ô/Ûà�N‰NÐ2°5°'Ð:Õ;ûð	ûð �N‰NÐ2°5°'Ð:Õ;ús)   ˜A ÁBÁ#BÁ9B ÂBÂB ÂB0Ú
schema_sqlc                 ó¸  — d}t        j                  | j                  d«      «      j                  «       }t	        j
                  t        «      5  ddlm}  |«       dz  dz  }t        j                  |j                  d¬«      «      }t        |t        «      r%|j                  d«      |k(  r|j                  d	«      nd}t        |t        «      r+t        d
„ |j!                  «       D «       «      r|cddd«       S ddd«       t#        j$                  d«      }	 |j'                  | «       i }|j)                  d«      j+                  «       D ]‚  \  }i }	|j)                  d|› d�«      j+                  «       }
|
D ]O  \  }}}}}}|r|gng }|r|s|j-                  d«       |�|j-                  d|› �«       dj/                  |«      |	|<   ŒQ |	||<   Œ„ 	 |j1                  «        |�¾t	        j
                  t        «      5  |j2                  j5                  dd¬«       t7        j8                  t;        |j2                  «      d¬«      \  }}t=        j>                  |dd¬«      5 }t        j@                  ||dœ|«       ddd«       t=        jB                  ||«       ddd«       |S |S # 1 sw Y   �ŒªxY w# |j1                  «        w xY w# 1 sw Y   ŒMxY w# 1 sw Y   |S xY w)u[  Expected columns per table: execute SCHEMA_SQL in an in-memory database and read
        PRAGMA table_info (no regex). Memoized on disk keyed by a DDL hash (~85ms per
        startup otherwise); only the reference-side parse is cached â€” diffing the LIVE
        database still runs every startup. A corrupt/stale cache degrades to recomputation.Núutf-8r   r   Úcachezschema_columns.json©ÚencodingÚschema_hashrY   c              3   ó€   K  — | ]6  }t        |t        «      xr  t        d „ |j                  «       D «       «      –— Œ8 y­w)c              3   ó<   K  — | ]  }t        |t        «      –— Œ y ­wr5   )rø   rÃ   )r1   Úvs     r)   r3   zESessionSchemaMixin._parse_schema_columns.<locals>.<genexpr>.<genexpr>Ê  s   è ø€ Ò.YÀa¬z¸!¼S×/AÑ.Yùs   ‚N)rø   r«   ÚallÚvalues)r1   rS   s     r)   r3   z;SessionSchemaMixin._parse_schema_columns.<locals>.<genexpr>É  s5   è ø€ ò 0Ø^b”
˜4¤Ó&ÒY¬3Ñ.YÈ4Ï;É;Ë=Ô.YÓ+YÓYñ0ùs   ‚<>z:memory:zNSELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'úPRAGMA table_info("ú")zNOT NULLzDEFAULT r%   T)ÚparentsÚexist_okz.schema_columns.)ÚdirÚprefixÚw)r@  rY   )"ÚhashlibÚsha256ÚencodeÚ	hexdigestrs   rt   r—   Úhermes_constantsr   r  ÚloadsÚ	read_textrø   r«   r±   rD  rE  r`   Úconnectr0  r^   r_   Úappendr'   ÚcloseÚparentÚmkdirÚtempfileÚmkstemprÃ   ÚosÚfdopenÚdumprJ   )r:  Ú
cache_pathr@  Ú_homeÚblobrY   ÚrefÚtable_columnsrâ   rS   rš   Ú_cidÚcol_nameÚcol_typeÚnotnullÚdefaultÚpkÚpartsÚfdÚtmpÚfhs                        r)   rU   z(SessionSchemaMixin._parse_schema_columns»  s›  € ð ˆ
Ü—n‘n Z×%6Ñ%6°wÓ%?Ó@×JÑJÓLˆÜ× Ñ ¤Ó+ñ 		åAÙ› 7Ñ*Ð-BÑBˆJÜ—:‘:˜j×2Ñ2¸GÐ2ÓDÓEˆDÜ+5°d¼DÔ+AÀdÇhÁhÈ}ÓF]ÐalÒFl�T—X‘X˜hÔ'ÐrvˆFÜ˜&¤$Ô'¬Cñ 0Øfl×fsÑfsÓfuô0ô -ð ÷		÷ 		ñ 		ô �o‰o˜jÓ)ˆð	Ø×Ñ˜jÔ)Ø79ˆMØŸ+™+Ø`óç‰h‹jò*‘�ð (*�Ø—{‘{Ð%8¸¸¸RÐ#@ÓA×JÑJÓL�ØFJò 5ÑB�D˜( H¨g°wÀá*2˜X™J¸�EÙ¡rØŸ™ ZÔ0ØÐ*ØŸ™ x°¨yÐ%9Ô:Ø%(§X¡X¨e£_�D˜’Nð5ð &*�˜cÒ"ñ*ð �I‰IŒKàÐ!Ü×$Ñ$¤YÓ/ñ ,Ø×!Ñ!×'Ñ'°¸tÐ'ÔDÜ"×*Ñ*¬s°:×3DÑ3DÓ/EÐN`Ôa‘��CÜ—Y‘Y˜r 3°Ô9ð Y¸RÜ—I‘I¨kÀ]ÑSÐUWÔX÷Yä—
‘
˜3 
Ô+÷,ð Ðˆ}Ð÷O		ñ 		ûð< �I‰I�Kú÷Yð Yú÷,ð Ðús?   ÁB!J!ÄB7J. Ç:A#KÉKÉ7KÊ!J+Ê.K ËK	ËKËKc           
      ó~  — | j                  t        «      }|j                  «       D ]|  \  }}	 |j                  d|› d�«      j	                  «       }|D �ch c]  }|d   ’Œ	 }}|j                  «       D ].  \  }}	||v rŒ	 |j                  d|› dt        |«      › d|	› �«       Œ0 Œ~ y	# t
        j                  $ r Y Œ•w xY wc c}w # t
        j                  $ re}
t        |
«      j                  «       }d|v rt        j                  d|||
«       Y d	}
~
ŒŸd
|v sd|v r‚ t        j                  d|||
«       Y d	}
~
ŒÅd	}
~
ww xY w)zžADD every SCHEMA_SQL column missing from the live tables (SCHEMA_SQL is the single
        source of truth; column additions need no version-gated migration).rF  rG  r8   zALTER TABLE "z" ADD COLUMN r%   zduplicate columnzreconcile %s.%s: %sNÚlockedÚbusyz;reconcile %s.%s failed; store remains behind SCHEMA_SQL: %s)rU   r   rX   r^   r_   r`   ra   rK   rÃ   ré   rc   rÛ   rd   )re   r\   Úexpectedrä   Údeclared_colsrf   r¡   Ú	live_colsrd  re  rj   Úmessages               r)   Ú_reconcile_columnsz%SessionSchemaMixin._reconcile_columnsì  sW  € ð ×-Ñ-¬jÓ9ˆØ)1¯©Ó)9ò 	Ñ%ˆJ˜ðØ—~‘~Ð(;¸J¸<ÀrÐ&JÓK×TÑTÓV�ð ,0Ö0 C˜˜Q›Ð0ˆIÐ0Ø&3×&9Ñ&9Ó&;ò Ñ"�˜(Ø˜yÑ(ØðØ—N‘N ]°:°,¸mÌBÈxËLÈ>ÐYZÐ[cÐZdÐ#eÕfñ	ñ	øô ×+Ñ+ò Ùðüò 1øô ×/Ñ/ò Ü! #›hŸn™nÓ.�GØ)¨WÑ4äŸ™Ð%:¸JÈÐRUÔVÜ Ø 7Ñ*¨f¸Ñ.?ð ä—N‘NØUÐWaÐckÐmp÷ñ ûðús5   ­#B&ÁB?Á>#CÂ&B<Â;B<ÃD<Ã5D7Ä!D7Ä7D<r=   c                 óÜ   — 	 | j                  d|› d�«      j                  «       }|syt	        d„ |D «       d„ ¬«      D �cg c]  }|d   ‘Œ	 c}S # t        j                  $ r d}Y Œ@w xY wc c}w )z�PRIMARY KEY column names of *table* in key order; None when the table is
        missing or has no columns (SCHEMA_SQL creates it correctly).rF  rG  Nc              3   ó,   K  — | ]  }|d    sŒ	|–— Œ y­w)é   Nr/   )r1   Úrs     r)   r3   z6SessionSchemaMixin._live_pk_columns.<locals>.<genexpr>  s   è ø€ Ò%>¨A¸¸1»¤aÑ%>ùs   ‚
�c                 ó   — | d   S )Nrw  r/   )rx  s    r)   rÐ   z5SessionSchemaMixin._live_pk_columns.<locals>.<lambda>  s   € ÈaÐPQÉd€ r+   )Úkeyr8   )r^   r_   r`   ra   rW   )r\   r=   rf   rx  s       r)   Ú_live_pk_columnsz#SessionSchemaMixin._live_pk_columns  ss   € ð	Ø—>‘>Ð$7¸°w¸bÐ"AÓB×KÑKÓMˆDñ Øä$Ñ%>°Ô%>ÁNÔSÖT˜��!“ÒTÐTøô ×'Ñ'ò 	ØŠDð	üò
 Us   ‚#A ¿A)ÁA&Á%A&Úlegacy_nameÚcopy_sqlc                 ó"  — | j                   }|j                  rt        j                  | |||||«       y| j	                  d«       	 t        j                  | |||||«       | j	                  d«       y# t
        $ r | j	                  d«       ‚ w xY w)u  RENAME *table* to *legacy_name*, CREATE it fresh from *ddl*, copy rows back with
        *copy_sql*, DROP the legacy copy, recreate *indexes* â€” as ONE write transaction.

        The writer connection is autocommit (``isolation_level=None``), so without an explicit
        BEGIN each statement commits on its own and a sibling process opening the same state.db
        between RENAME and CREATE runs SCHEMA_SQL's ``CREATE TABLE IF NOT EXISTS`` first: our
        CREATE then fails with "table already exists", every row is stranded in *legacy_name*
        and the live table is empty. BEGIN IMMEDIATE holds the write lock for the whole rebuild
        so the sibling blocks (and retries under its lock patience) instead of interleaving;
        any failure rolls the RENAME back.NzBEGIN IMMEDIATEÚROLLBACKÚCOMMIT)Ú
connectionÚin_transactionrT   Ú_rebuild_table_statementsr^   rÈ   )r\   r=   r|  r§   r}  ÚindexesÚconns          r)   Ú_rebuild_tablez!SessionSchemaMixin._rebuild_table  s‘   € ð × Ñ ˆØ×ÒÜ×8Ñ8¸ÀÈÐUXÐZbÐdkÔlØØ�‰Ð(Ô)ð	Ü×8Ñ8¸ÀÈÐUXÐZbÐdkÔlð 	�‰�xÕ øô ò 	Ø�N‰N˜:Ô&Øð	ús   ÁA2 Á2Bc                 óÎ   — | j                  d|› d|› �«       | j                  |«       | j                  |«       | j                  d|› �«       |D ]  }| j                  |«       Œ y )NzALTER TABLE z RENAME TO zDROP TABLE rÎ   )r\   r=   r|  r§   r}  r„  r…   s          r)   rƒ  z,SessionSchemaMixin._rebuild_table_statements0  sa   € à�‰˜ e W¨K¸°}ÐEÔFØ�‰�sÔØ�‰�xÔ Ø�‰˜ [ MÐ2Ô3Øò 	 ˆCØ�N‰N˜3Õñ	 r+   c                 ó’   — | j                  |d«      }|�|ddgk(  ryt        j                  d|«       | j                  |dddd«       y)	a  Rebuild ``gateway_routing`` when its PRIMARY KEY predates scoping (``session_key TEXT
        PRIMARY KEY``): the reconciler ADDs ``scope`` but SQLite cannot ALTER a PK, so every
        routing write fails (ON CONFLICT mismatch / cross-scope UNIQUE violation). Newest
        row wins a cross-scope session_key collision (INSERT OR REPLACE in updated_at order).

        Early builds of the routing-index migration (#59203) created the table with ``session_key TEXT
        PRIMARY KEY`` and no ``scope`` column. ``_reconcile_columns()`` ADDs the missing ``scope`` column on
        those databases, but SQLite cannot ALTER a primary key, so the shipped composite ``PRIMARY KEY
        (scope, session_key)`` never lands. On such tables every write path is broken:
        Úgateway_routingNÚscopeÚsession_keyz]gateway_routing has legacy primary key %r; rebuilding with composite (scope, session_key) keyÚgateway_routing_legacy_pkzÄCREATE TABLE gateway_routing (
    scope TEXT NOT NULL DEFAULT '',
    session_key TEXT NOT NULL,
    entry_json TEXT NOT NULL,
    updated_at REAL NOT NULL,
    PRIMARY KEY (scope, session_key)
)zÊINSERT OR REPLACE INTO gateway_routing (scope, session_key, entry_json, updated_at) SELECT COALESCE(scope, ''), session_key, entry_json, updated_at FROM gateway_routing_legacy_pk ORDER BY updated_at ASC)r{  rc   rš   r†  )re   r\   Úpk_colss      r)   Ú_heal_gateway_routing_pkz+SessionSchemaMixin._heal_gateway_routing_pk9  sa   € ð ×'Ñ'¨Ð0AÓBˆØˆ?˜g¨'°=Ð)AÒAØÜ�‰ØkÐmtô	
ð 	×ÑØÐ%Ð'BððEõ	
r+   c                 óŽ  — | j                  |d«      }|�d|v ryt        j                  dt        |«      «       |j	                  d«       	 | j                  |ddt        dt        «       |j	                  d	«       y# t        j                  $ r }t        j                  d|«       Y d}~Œ@d}~ww xY w# |j	                  d	«       w xY w)
u   Rebuild ``session_model_usage`` when its PRIMARY KEY lacks ``task``: installs already at
        v22+ when ``task`` landed carry the 5-column PK, the reconciler ADDs ``task`` but
        SQLite cannot ALTER a PK, and the v22 rebuild is unreachable â€” every upsert then
        fails (ON CONFLICT mismatch), silently zeroing accounting. Idempotent. FK-off
        window: INSERT OR IGNORE does NOT suppress FK violations, so an orphaned usage row
        would abort the rebuild (PRAGMA foreign_keys is a no-op inside a transaction; none
        is open here). OR IGNORE: COALESCE(task, '') on legacy NULL rows can collide with a
        genuine ''-task row â€” keep the first.

        Installs whose ``state.db`` reached ``schema_version >= 22`` before the ``task`` dimension was added
        carry a 5-column PRIMARY KEY ``(session_id, model, billing_provider, billing_base_url,
        billing_mode)``. See #73823.
        Úsession_model_usageNÚtaskzdsession_model_usage has legacy primary key %r (missing task); rebuilding with composite 6-column keyzPRAGMA foreign_keys=OFFÚsession_model_usage_legacy_pka  INSERT OR IGNORE INTO session_model_usage (
                       session_id, model, billing_provider, billing_base_url,
                       billing_mode, task, api_call_count, input_tokens,
                       output_tokens, cache_read_tokens, cache_write_tokens,
                       reasoning_tokens, estimated_cost_usd, actual_cost_usd,
                       cost_status, cost_source, first_seen, last_seen
                   )
                   SELECT session_id, model,
                          COALESCE(billing_provider, ''),
                          COALESCE(billing_base_url, ''),
                          COALESCE(billing_mode, ''),
                          COALESCE(task, ''),
                          api_call_count, input_tokens,
                          output_tokens, cache_read_tokens, cache_write_tokens,
                          reasoning_tokens, estimated_cost_usd, actual_cost_usd,
                          cost_status, cost_source, first_seen, last_seen
                   FROM session_model_usage_legacy_pkz'session_model_usage PK heal skipped: %szPRAGMA foreign_keys=ON)r{  rc   rš   rW   r^   r†  Ú_SESSION_MODEL_USAGE_HEAL_DDLÚ_SESSION_MODEL_USAGE_INDEX_SQLr`   ra   rÛ   )re   r\   r�  rj   s       r)   Ú_heal_session_model_usage_pkz/SessionSchemaMixin._heal_session_model_usage_pkX  s¿   € ð ×'Ñ'¨Ð0EÓFˆØˆ?˜f¨Ñ/ØÜ�‰ØrÜ�7‹Oô	
ð 	�‰Ð0Ô1ð 	5Ø×ÑØÐ-Ð/NÔPmð9ô" /ô5ð> �N‰NÐ3Õ4øô ×'Ñ'ò 	IÜ�L‰LÐBÀC×HÑHûð	Iûð �N‰NÐ3Õ4ús*   ÁA; Á;B.ÂB)Â$B1 Â)B.Â.B1 Â1Cc                 óZ  — t        dd¬«       | j                  j                  «       }|j                  t        «       | j                  |«       | j                  |«       | j                  |«       	 |j                  d«       | j                  |t        «       t        j                   t        j                  «      5  |j                  d«      j#                  «       �|j                  d«       ddd«       | j%                  |«      }|j                  d	t&        f«      j#                  «       }|du| _        | j(                  r| j+                  |«       |s| j-                  |«       |j                  d
«      j#                  «       }|€�|j                  dt.        f«       t0        j0                  j3                  t0        j4                  j6                  «      j9                  «       }|j;                  ddt=        t?        j@                  «       «      fd|fg«       n| jC                  ||d   |«       | jE                  |«       |r| jG                  |«       | j                  jI                  «        y# t        j                  $ r!}t        j                  d|«       Y d}~�Œd}~ww xY w# 1 sw Y   �Œ¶xY w)zöCreate tables and FTS if missing, reconcile columns, run data migrations. Column
        additions are declarative via _reconcile_columns(), so reordered migrations can
        never skip a column; schema_version remains for data migrations only.ç     À‚@Ústate_db_init_schema©ÚphasezŠCREATE INDEX IF NOT EXISTS idx_messages_platform_msg_id ON messages(session_id, platform_message_id) WHERE platform_message_id IS NOT NULLz/idx_messages_platform_msg_id create skipped: %sNz3SELECT 1 FROM messages WHERE active IS NULL LIMIT 1z3UPDATE messages SET active = 1 WHERE active IS NULLz.SELECT 1 FROM state_meta WHERE key = ? LIMIT 1z*SELECT version FROM schema_version LIMIT 1z/INSERT INTO schema_version (version) VALUES (?)z;INSERT OR IGNORE INTO state_meta (key, value) VALUES (?, ?)Ústore_instance_idÚstore_created_at_utcr   )%r	   r  r\   r0  r   rt  rŽ  r•  r^   r`   ra   rc   rÛ   r¸   r   rs   rt   r‚   ro   r   r  rv   rr   r   Údatetimer	  ÚtimezoneÚutcÚ	isoformatÚexecutemanyrÃ   ÚuuidÚuuid4Ú_run_data_migrationsÚ_ensure_unique_title_indexÚ	_init_ftsr%  )re   r\   rj   Úfts5_availableÚ	stale_rowr¡   Únow_isos          r)   Ú_init_schemazSessionSchemaMixin._init_schema’  s?  € ô 	  Ð-CÕDØ—‘×"Ñ"Ó$ˆØ×ÑœZÔ(ð 	×Ñ Ô'Ø×%Ñ% fÔ-ð 	×)Ñ)¨&Ô1ð	QØ�N‰Nðeôð 	×3Ñ3°FÔ<NÔOô × Ñ ¤×!9Ñ!9Ó:ñ 	VØ�~‰~ÐSÓT×]Ñ]Ó_ÐkØ—‘ÐTÔU÷	Vð ×3Ñ3°FÓ;ˆØ—N‘NÐ#SÔVcÐUeÓf×oÑoÓqˆ	Ø#¨4Ð/ˆŒØ�?Š?à×'Ñ'¨Ô/Ùð ×#Ñ# FÔ+à�n‰nÐIÓJ×SÑSÓUˆØˆ;Ø�N‰NÐLÌ~ÐN_Ô`ô ×'Ñ'×+Ñ+¬H×,=Ñ,=×,AÑ,AÓB×LÑLÓNˆGØ×ÑØMØ%¤s¬4¯:©:«<Ó'8Ð9Ð<RÐT[Ð;\Ð]õð
 ×%Ñ% f¨c°!©f°nÔEà×'Ñ'¨Ô/ÙØ�N‰N˜6Ô"Ø�
‰
×ÑÕøôU ×'Ñ'ò 	QÜ�L‰LÐJÈC×PÒPûð	Qú÷	Vñ 	Vús$   Á1I) Â;1J É)JÉ<JÊJÊ J*Úcurrent_versionr§  c           
      óì  — t        dd¬«       |dk  r‚t        j                  t        j                  «      5  |j                  dt        dd«      › dt        d	«      › �«       |j                  d
t        dd«      › dt        dd«      › d�«       ddd«       |dk  r	 | j                  |«       |dk  rAt        j                  t        j                  «      5  |j                  t        «       ddd«       |dk  r| j                  |«       |dk  r'|r%| j                  |«      r| j                  dd|¬«       |dk  r| j!                  |«       d}|dk  r|r| j#                  |«      }|r¡| j                  |«      s�|j                  d«      j%                  «       €q| j'                  |«      s`| j)                  |«      sO|j                  dt+        t,        «      f«      j%                  «       €!| j                  dt+        t,        «      |¬«       |t.        k  r|r|r|j                  dt.        f«       yyyy# 1 sw Y   �Œ¤xY w# t        $ r!}t        j                  d|«       Y d}~�Œµd}~ww xY w# 1 sw Y   �Œ�xY w)z¨Version-gated chain for DATA migrations only (row backfills); column additions never
        belong here. Advances schema_version at the end unless FTS5 is unavailable.r—  Ústate_db_data_migrationsr™  r,  z™UPDATE sessions SET model_config = json_set(COALESCE(model_config, '{}'), '$._delegate_from', parent_session_id) WHERE parent_session_id IS NOT NULL AND Úmodel_configz$._delegate_fromz IS NULL AND Úsessionsz’UPDATE sessions SET model_config = json_set(COALESCE(model_config, '{}'), '$._delegate_from', '__orphaned__') WHERE parent_session_id IS NULL AND z$._branched_fromzý IS NULL AND title IS NULL AND message_count <= 25 AND EXISTS (SELECT 1 FROM messages m             WHERE m.session_id = sessions.id AND m.role = 'tool') AND NOT EXISTS (SELECT 1 FROM sessions ch                 WHERE ch.parent_session_id = sessions.id)Né   z)v18 gateway metadata backfill skipped: %sr-  é   é   Úfts_optimize_availablerÖ   rÏ   é   Té   zESELECT 1 FROM state_meta WHERE key = 'fts_rebuild_high_water' LIMIT 1zPSELECT 1 FROM state_meta WHERE key = 'fts_storage_version' AND value = ? LIMIT 1r¾   z%UPDATE schema_version SET version = ?)r	   rs   rt   r`   ra   r^   r   r   Ú-_backfill_gateway_metadata_from_sessions_jsonr—   rc   rÛ   Ú!_SESSION_MODEL_USAGE_V20_SEED_SQLÚ _migrate_v22_session_model_usageÚ_db_needs_fts_storage_upgraderÚ   rk   rÔ   r‚   Ú_has_fts_trashÚ'_fts_external_index_empty_with_messagesrÃ   r   r   )re   r\   r«  r§  rj   Úfts_migrations_completes         r)   r¤  z'SessionSchemaMixin._run_data_migrationsæ  s‚  € ô 	  Ð-GÕHð ˜RÒä×$Ñ$¤W×%=Ñ%=Ó>ñ Ø—‘ðô -¨^Ð=OÓPÐQð RÜ/°
Ó;Ð<ð	>ôð —‘ðä,¨^Ð=OÓPÐQð RÜ,¨^Ð=OÓPÐQð RPðPô	÷ð$ ˜RÒðOð ×BÑBÀ6ÔJð ˜RÒä×$Ñ$¤W×%=Ñ%=Ó>ñ BØ—‘Ô@ÔA÷Bà˜RÒØ×1Ñ1°&Ô9ð ˜RÒ¡N°t×7YÑ7YÐZ`Ô7aØ�M‰MÐ2°CÀˆMÔGØ˜RÒà×.Ñ.¨vÔ6Ø"&ÐØ˜RÒ¡Nð '+×&JÑ&JÈ6Ó&RÐ#ñ. Ø×6Ñ6°vÔ>Ø—‘ØWóç‰h‹jð!ð ×'Ñ'¨Ô/Ø×@Ñ@ÀÔHð �~‰~ØbÜÔ(Ó)Ð+ó÷ ‰h‹jð!ð —‘Ð3´SÔ9LÓ5MÐV\�Ô]ð
 œ^Ò+Ñ0GÉNØ�N‰NÐBÄ^ÐDUÕVð M[Ð0GÐ+÷sñ ûô0 ò OÜ—‘ÐHÈ#×NÒNûðOú÷Bñ Bús0   ¶AH/ÂH< ÃI)È/H9È<	I&ÉI!É!I&É)I3c                 óø   — 	 |j                  d«      j                  «       d   }|ry| j                  |ddt        dt        «       y# t
        j                  $ r }t        j                  d|«       Y d}~yd}~ww xY w)u£   v22: ``task`` joins the session_model_usage PRIMARY KEY ('' = main loop; aux calls
        named). SQLite cannot ALTER a PK, so rebuild; existing rows â†’ task=''.z\SELECT COUNT(*) FROM pragma_table_info('session_model_usage') WHERE name = 'task' AND pk > 0r   Nr�  Úsession_model_usage_v21aõ  INSERT INTO session_model_usage (
                                   session_id, model, billing_provider, billing_base_url,
                                   billing_mode, task, api_call_count, input_tokens,
                                   output_tokens, cache_read_tokens, cache_write_tokens,
                                   reasoning_tokens, estimated_cost_usd, actual_cost_usd,
                                   cost_status, cost_source, first_seen, last_seen
                               )
                               SELECT session_id, model, billing_provider, billing_base_url,
                                      billing_mode, '', api_call_count, input_tokens,
                                      output_tokens, cache_read_tokens, cache_write_tokens,
                                      reasoning_tokens, estimated_cost_usd, actual_cost_usd,
                                      cost_status, cost_source, first_seen, last_seen
                               FROM session_model_usage_v21z+v22 session_model_usage rebuild skipped: %s)	r^   r‚   r†  Ú_SESSION_MODEL_USAGE_V22_DDLr”  r`   ra   rc   rÛ   )re   r\   Ú	legacy_pkrj   s       r)   r¸  z3SessionSchemaMixin._migrate_v22_session_model_usageO  s�   € ð	Mð
 Ÿ™Ønóç‰h‹j˜ñˆIñ ØØ×ÑØÐ-Ð/HÔJfð?ô /õøô" ×'Ñ'ò 	MÜ�L‰LÐFÈ×LÑLûð	Mús   ‚$A §A ÁA9ÁA4Á4A9c                 óv  — 	 |j                  t        «       y# t        j                  $ ry 	 |j                  d«       t        j                  d|j                  «       |j                  t        «       Y y# t        j                  $ r t        j                  d«       Y Y yw xY wt        j                  $ r Y yw xY w)z³Unique title index. Older DBs may hold duplicate aliases from before the constraint;
        the newest keeps the alias. Must never abort opening the DB, so the repair is guarded.ag  UPDATE sessions AS older
                       SET title = NULL
                       WHERE title IS NOT NULL
                         AND EXISTS (
                             SELECT 1 FROM sessions AS newer
                             WHERE newer.title = older.title
                               AND newer.rowid > older.rowid
                         )zFCleared %d duplicate session title(s) while restoring the unique indexzICould not repair duplicate session titles; unique title index not createdN)
r^   Ú_TITLE_UNIQUE_INDEX_SQLr`   ÚIntegrityErrorrc   rd   Úrowcountr÷   r™   ra   rÜ   s     r)   r¥  z-SessionSchemaMixin._ensure_unique_title_indexp  sŸ   € ð	Ø�N‰NÔ2Õ3øÜ×%Ñ%ò 	nðnØ—‘ð  ô ô —‘Ø\Ð^d×^mÑ^môð —‘Ô6Ö7øÜ—=‘=ò nÜ× Ñ Ð!l×mðnúä×'Ñ'ò 	Ùð	ús-   ‚ ˜B8¬AA4Á4(B ÂB8ÂB Â B8Â7B8c                 ó  ‡ ‡‡‡‡‡	— ‰ j                  ‰«      Št        ‰d«      }‰ j                  s‰ j                  ‰‰¬«       ‰ j                  r;‰ j	                  ‰‰¬«      r‰ j                  ‰«       nôdx‰ _        x‰ _        ‰ _        nÞt        ‰   \  ŠŠ	‰ j                  ‰t        «      xs d|v }‰ j                  ‰t        «      xs d|v }d	ˆˆˆˆ ˆ	fd„}|r‰ j                  ‰|«       nZ‰ j                  ‰d‰«      ‰ _        ‰ j                  r6‰ j                  ‰d‰	«      Š‰‰ _        ‰r|r‰ j                  ‰ˆˆˆ ˆfd„«       ‰ j                  r‰s|s‰ j                  ‰«       ‰ j                  r‰ j                  ‰«       yy)
a\  Create/repair the FTS objects on an FTS5-capable runtime. The DDL runs even when the
        vtable exists so CREATE TRIGGER IF NOT EXISTS repairs trigger-only degradation.
        OPT-IN v23 boundary: a legacy v22 inline install keeps its inline schema + triggers
        (the v23 DDL would create the trigram source VIEW and leave a mixed state).)r‘   r’   Úmessages_fts_cjkr  Fr‘   r’   Nc                  óä   •— ‰j                  ‰d‰ «      ‰_        ‰j                  sy ‰j                  ‰d‰«      ‰_        ‰j                  ‰‰‰j                  ¬«       ‰s‰j	                  ‰«       y y )Nr‘   r’   rÝ   )r•   r2  r3  rã   r–   )rŸ   r\   Ú
legacy_ftsre   r    s   €€€€€r)   Úensure_and_rebuildz8SessionSchemaMixin._init_fts.<locals>.ensure_and_rebuild§  sx   ø€ Ø$(×$;Ñ$;¸FÀNÐT\Ó$]�Ô!Ø×(Ò(ØØ*.×*AÑ*AÀ&ÐJ`ÐbmÓ*n�Ô'Ø×)Ñ)Ø :¸t×?VÑ?Vð *ô ñ "Ø×/Ñ/°Õ7ð "r+   c                  ó,   •— ‰j                  ‰ ‰‰¬«      S )NrÝ   )rã   )r\   rÈ  re   Útrigram_enableds   €€€€r)   rÐ   z.SessionSchemaMixin._init_fts.<locals>.<lambda>À  s   ø€  D×$=Ñ$=Ø &¨zÈ?ð %>ó %€ r+   rÆ   )r“   r   r  rÊ   r  r–   r2  r3  rÙ   r”   r†   r¿   r6   rÉ   r•   r¢   )
re   r\   Úorphan_repairedÚbase_triggers_missingÚtrigram_triggers_missingrÉ  rŸ   rÈ  rË  r    s
   ``    @@@@r)   r¦  zSessionSchemaMixin._init_ftsˆ  s�  ý€ ð
 ×3Ñ3°FÓ;ˆ
ô 9ØÐPó
ˆð �ŠØ×/Ñ/°¸zÐ/ÔJØ�?Š?Ø×&Ñ& v°jÐ&ÔAà×+Ñ+¨FÕ3àX]Ð]�Ô!Ð] DÔ$;¸dÕ>Uä$,¨ZÑ$8Ñ!ˆH�kð ×*Ñ*¨6Ô3EÓFÒkÈ.Ð\kÐJkð "ð ×*Ñ*¨6Ô3HÓIÒvÐMcÐgvÐMvð %÷	8ñ 	8ñ %ð ×2Ñ2°6Ð;MÕNà$(×$;Ñ$;¸FÀNÐT\Ó$]�Ô!Ø×$Ò$à&*×&=Ñ&=¸fÐF\Ð^iÓ&j�OØ.=�DÔ+Ù&Ñ+CØ×:Ñ:Ø"öôð × Ò ©Ñ<Qà×+Ñ+¨FÔ3à×ÒØ×3Ñ3°FÕ;ð r+   c                 ó:  — t        | j                  «      5 }|r |«        	 ddd«       y	 ddd«       t        j                  d«       |j	                  t
        t        f«       | j                  |«       d| _        dx| _	        x| _
        | _        y# 1 sw Y   ŒgxY w)aß  Run FTS bootstrap or trigger-repair rebuild under cross-process admission.

        The fresh/base-missing path includes DDL in ``rebuild_fn`` so no process can publish
        triggers before it owns the rebuild. Other repair paths may already have recreated an
        optional trigger; deferral therefore still drops every trigger and persists the stale
        breadcrumb. A later recovery path restores the complete family.

        See #93200.
        See #105790.
        Nz Deferred startup FTS rebuild: another process holds the rebuild authority for this state.db; detaching FTS sync until the stale-index recovery path rebuilds it.TF)r   r  rc   rd   r^   Ú_STALE_KEY_UPSERT_SQLr   rv   r  r2  r3  rÙ   )re   r\   Ú
rebuild_fnr  s       r)   rÉ   z0SessionSchemaMixin._run_admitted_startup_rebuildË  s”   € ô # 4§<¡<Ó0ð 	°HÙÙ”Ø÷	ð 	Ø÷	ô 	�‰ðwô	
ð 	�‰Ô,¬}Ð.>Ô?Ø×#Ñ# FÔ+ØˆŒØPUÐUˆÔÐU˜DÔ3°dÕ6M÷	ð 	ús   –BÂBc                 ó@  — t        «       dz  dz  }|j                  «       syt        |dd¬«      5 }t        j                  |«      }ddd«       t        t        «      sy|j                  «       D �]  \  }}t        |«      j                  d«      st        |t        «      sŒ2|j                  d«      }|sŒF|j                  d	«      }t        |t        «      r|nd}	|j                  d
|j                  d«      xs ||	�|	j                  d«      nd|j                  d«      |	�|	j                  d«      nd|j                  d«      |	�t        j                  |«      nd|j                  d«      s|j                  d«      rdndt        |«      f«       �Œ" y# 1 sw Y   �ŒOxY w)uŽ   One-time v18 backfill of gateway metadata from sessions.json. Only fills NULL
        columns â€” never overwrites data written by newer code.r¯  zsessions.jsonNrx  r<  r>  r~   rg   Úoriginai  UPDATE sessions
                   SET session_key = COALESCE(session_key, ?),
                       chat_id = COALESCE(chat_id, ?),
                       chat_type = COALESCE(chat_type, ?),
                       thread_id = COALESCE(thread_id, ?),
                       display_name = COALESCE(display_name, ?),
                       origin_json = COALESCE(origin_json, ?),
                       expiry_finalized = CASE
                           WHEN COALESCE(expiry_finalized, 0) = 0 AND ? = 1 THEN 1
                           ELSE expiry_finalized
                       END
                   WHERE id = ?r‹  Úchat_idÚ	chat_typeÚ	thread_idÚdisplay_nameÚexpiry_finalizedÚmemory_flushedr8   r   )r   ÚexistsÚopenr  Úloadrø   r«   rX   rÃ   r¯   r±   r^   r  )
re   r\   Úsessions_fileÚfÚdatarz  Úentryrg   rÓ  Úorigin_dicts
             r)   r¶  z@SessionSchemaMixin._backfill_gateway_metadata_from_sessions_jsonã  sm  € ô (Ó)¨JÑ6¸ÑHˆØ×#Ñ#Ô%ØÜ�- ¨wÔ7ð 	 ¸1Ü—9‘9˜Q“<ˆD÷	 ä˜$¤Ô%ØØŸ*™*›,ó 	‰JˆC�Ü�3‹x×"Ñ" 3Ô'¬z¸%ÄÔ/FØØŸ™ <Ó0ˆJÙØØ—Y‘Y˜xÓ(ˆFÜ$.¨v´tÔ$<™&À$ˆKØ�N‰Nð#ð —I‘I˜mÓ,Ò3°ÐS^ÐSj°[·_±_ÀYÔ5OÐptØ—I‘I˜kÓ*ÈKÐLc¨K¯O©O¸KÔ,HÐimØ—I‘I˜nÓ-À[ÐE\¬t¯z©z¸&Ô/AÐbfØŸ™Ð#5Ô6¸%¿)¹)ÐDTÔ:U‘AÐ[\Ô^aÐblÓ^mð	öñ	÷		 ñ 	 ús   °FÆF)r/   rÆ   )0Ú__name__Ú
__module__Ú__qualname__Ú__doc__r`   ÚCursorrk   r  ro   rv   Ústaticmethodr   rÃ   r†   r   r‹   r�   r¢   r¦   r¸   rº   rÊ   rÇ   rÔ   r˜   rã   rÒ   r  r  r)  r  r.  r   rU   rt  r   r{  r†  rƒ  rŽ  r•  rª  r¤  r¸  r¥  r¦  rÉ   r¶  r/   r+   r)   rT   rT   ±   sr  „ ÙHð°G·N±Nð Àtó ð0	¨G¯N©Nð 	¸tó 	ðD¨W¯^©^ð DÀó Dð ðQ g§n¡nð Q¸XÀc¹]ð QÈtò Qó ðQð ðS°¸#±ð SÀ4ò Só ðSð*¸¿¹ð *ÈCó *ðX ð
L°·±ð 
LÀ4ò 
Ló ð
Lð ð G°w·~±~ð  GÈCð  GÐTXò  Gó ð GðD ð	K°'·.±.ð 	KÀsð 	KÈtò 	Kó ð	Kð%=°W·^±^ð %=ÐPTð %=ÐY]ó %=ðN ð! W§^¡^ð !¸3ð !À4ò !ó ð!ð
°g·n±nð Èó ð8gÀÇÁð gÐSWó gð ØGLÐfjò 7 W§^¡^ð 7Àð 7Ð_cð 7Ðosò 7ó ð7ð w§~¡~ð À3ð È8ÐTXÉ>ó ðBL°7·>±>ð LÐW[ó Lð\ [_ò I¨¯©ð IÀDð IÐcgó Ið ;¨Tó ;ðz/°·±ð /È4ð /ÐTXó /ðb°7·>±>ð Àdó ð" ð.¨#ð .°$°s¸DÀÀcÀ¹NÐ7JÑ2Kò .ó ð.ð`¨¯©ð ¸Dó ð> ð
U §¡ð 
U¸ð 
UÀÈÈcÉÑ@Sò 
Uó ð
Uð ñ!˜wŸ~™~ð !°cð !Èð !ÐRUð !Ðadð !Ðuyò !ó ð!ð. ò ó ð ð
¨w¯~©~ð 
À$ó 
ð>65°7·>±>ð 65Àdó 65òtRðhgW¨7¯>©>ð gWÈCð gWÐaeð gWÐjnó gWðRM°w·~±~ð MÈ$ó MðB°·±ð ÀDó ð0A< §¡ð A<°4ó A<óFVð0%ÀGÇNÁNð %ÐW[ô %r+   rT   r…  c                 óž   — | j                  t        «       t        j                  t        «      }|j                  | j                  «       «       y)a¿  Bring a raw ``state.db`` connection to the canonical SCHEMA_SQL shape.

    Single durable-shape authority for callers that open ``state.db``
    outside SessionDB. The async-delegation tool used to carry its own
    CREATE TABLE and ALTER column list for ``async_delegations``; it drifted
    from SCHEMA_SQL (same-name columns with different nullability/defaults
    depending on which authority touched the database first, #94691). This
    helper instead replays the canonical DDL (every statement is
    IF NOT EXISTS/idempotent) and reuses SessionDB's declarative column
    reconciliation, so out-of-band openers can never grow a second
    hand-maintained shape for the same durable tables.
    N)r0  r   ÚobjectÚ__new__rT   rt  r\   )r…  Úshims     r)   Úreconcile_state_schemarì    s7   € ð 	×Ñ”zÔ"ô
 �>‰>Ô,Ó-€DØ×Ñ˜DŸK™K›MÕ*r+   )Qrå  rs   r�  rM  Úloggingr  r[  r`   rY  rö   r¢  Útypingr   r   r   r   rQ  r   Úhermes_startup_watchdogr	   Úutilsr
   Úhermes_state_commonr   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   Úhermes_state_ftsr   Úhermes_state_holdersr   Ú	getLoggerrc   rü   rý   r  r  r  r#  r�   rÃ   r*   r,   rV   Ú__annotations__r6   r¿   r”   rá   r”  r“  r'   r¬   r®   r¿  r·  rÂ  rÐ  rÂ   rÀ   rÁ   r  rF   rK   rZ   rT   Ú
Connectionrì  )Úlns   0r)   ú<module>rø     s$  ðòó Û Û Û Û Û 	Û Û Û Û ß 1Ó 1õ -Ý ;Ý !÷÷ ÷ ÷ õ õ <Ý 0ð 
ˆ×	Ñ	˜>Ó	*€à !Ð Ø#Ð ð !Ð Ø#Ð ð  Ð Ø%Ð ðE˜ð E ó Eð
 +/Ð ˜ ™Ó .ñ
 ÑK¨ÔKÓKÐ ÙÑV mÔVÓVÐ ð ˜_Ð-°nÐF\Ð5]Ñ^€àað ð"Ð ð!Ð ð,  $Ÿy™yØ"×-Ñ-Ó/°Ñ2Ð3Ø'D×'OÑ'OÓ'QÐRSÐTVÐ'WÖX €x�"—(‘(“*ÓÒXñYàÐñó Ð ð%HÐ !ð< mð ð pð ð nð ð vÐ ð %lÐ !ð RWò  sð °Cð È$ð Ð[^ó ð0ˆcð 0�có 0ð
" eó "÷"Wñ Wðt"+ ×!3Ñ!3ð +¸ô +ùò]% Ys   ÄE9
