+
    „WÝjKD ã            	       ó°  a€ R0 t\0 t R t^ RIt^ RIt^ RIt^ RIt^ RIt^ RIt^ RIt^ RI	t	^ RI
t
^ RIt^ RIHtHtHtHt ^ RIHt ^ RIHt ^ RIHt ^ RIHtHtHtHtHtHtHtHtH t H!t!H"t"H#t#H$t$H%t%H&t&H't'H(t( ^ RI)H*t*H+t+H,t,H-t- ^ RI.H/t/ ^ R	I0H1t1H2t2 ^ R
I3H4t4 ^ RI5H6t6 ]Pn                  ! R4      t8^t9Rt:^
t;Rt<Rt=Rt>Rt?Rt@RtARtBR R ltCRsD] ^ k ]E;QJ d    . R ]% 4       F  NK  	  5M! R ]% 4       4      tF]E;QJ d    . R ]% 4       F  NK  	  5M! R ]% 4       4      tGR]]3R] ]!3/tHRtIR1tJRtKRP™                  ]KP›                  4       ^ ,          .]KP›                  4       ^R2  U u. uF  p R3V P�                  4       ,           NK  	  up ,           R4.,           4      tORtPRtQRtRR tSR!tTR"tUR#R/R$ R% lltVR& R' ltWR( R) ltX ! R* R+4      tYR, R- ltZR. R/ lt[R# u up i )5zô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Úbase_index_recognisedÚbase_source_view_is_staleÚfts_layout_current)Ú_read_proc_argv)Ú_delete_metaÚ	_meta_row)Úis_sqlite_lock_error)Únext_title_in_lineageÚhermes_stateç      N@g      œ@g      ¬@Úmessage_uid_backfillÚmessage_uid_backfill_idiÐ  ç      ð?c                ó0   € V ^8„  d   QhR\         R\        /# )é   ÚpidÚreturn)ÚintÚstr)Úformats   "ÚGC:\Users\johnn\AppData\Local\hermes\hermes-agent\hermes_state_schema.pyÚ__annotate__r1   8   s   € ÷ Eñ Eœð E¤ñ Eó    c                 ó\   € \        V 4      pV'       d   R P                  V4      R,          # R# )Ú :Néx   Nz<cmdline unavailable>)r   Újoin)r+   Úargvs   & r0   Ú_holder_cmdliner8   8   s'   € Ü˜3Ó€Dß#'ˆ3�8‰8�D‹>˜$ÕÐDÐ-DÐDr2   c              #   ó8   "  € T F  pR V9   g   K  Vx € K  	  R# 5i)Ú	_trigram_N© ©Ú.0Úns   & r0   Ú	<genexpr>r?   B   s   é € ÐK© A¸+ÈÑ:JŸaša«ùs   ‚	�
c              #   ó>   "  € T F  q\         9  g   K  Vx € K  	  R # 5i©N)Ú_FTS_TRIGRAM_TRIGGERSr<   s   & r0   r?   r?   C   s   é € ÐV¡m Ô@UÑ7UŸ1š1£mùs   ‚“
FTz[COALESCE(content, '') || ' ' || COALESCE(tool_name, '') || ' ' || COALESCE(tool_calls, '') 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)
)Ú
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'Údelete_firstc                óH   € V ^8„  d   QhR\         R\        R\        R\         /# )r*   ÚtableÚindentrE   r,   )r.   r-   Úbool)r/   s   "r0   r1   r1   –   s)   € ÷ ñ ¤sð ´Cð Ì$ð Ô[^ñ r2   c               óv   € RV,          pV'       d	   V RV  R2MRpRV V RV  RV RV R	V R
V RV RVRR  2# )zNLegacy inline (pre-v23) FTS re-population script fragment (whitespace pinned).r4   úDELETE FROM z;
Ú rC   úINSERT INTO z(rowid, content)
zSELECT id,
z'       COALESCE(content, '') || ' ' ||
z)       COALESCE(tool_name, '') || ' ' ||
z        COALESCE(tool_calls, '')
zFROM messages;
Néüÿÿÿr;   )rG   rH   rE   ÚpadÚbodys   &&$  r0   Ú_legacy_inline_reinsert_sqlrQ   –   sp   € à
��,€Cß-9ˆcˆU�,˜u˜g SÑ)¸r€Dà
ˆTˆF�3�%�| E 7Ð*<Øˆ%ˆ|Øˆ%Ð7Øˆ%Ð9Øˆ%Ð0Øˆ%Ð  C R ˜zð	+ðr2   c                ó0   € V ^8„  d   QhR\         R\         /# )r*   Úidentr,   )r.   )r/   s   "r0   r1   r1   ¤   s   € ÷ 0ñ 0Œcð 0”cñ 0r2   c                óB   € RV P                  RR4      ,           R,           # )zDouble-quote an SQL identifier.Ú"z"")Úreplace)rS   s   &r0   Ú_qrW   ¤   s   € à�—‘˜s DÓ)Õ)¨CÕ/Ð/r2   c                ó$   € V ^8„  d   QhR\         /# ©r*   r,   )Útuple)r/   s   "r0   r1   r1   ©   s   € ÷ "ñ "¤eñ "r2   c                 ó  € \         ft   \        P                  \        4      p \        ;QJ d+    . R \        V P                  4       4       4       F  NK  	  5M$! R \        V P                  4       4       4       4      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   ó’   a"  € T F<  w  opR P                  RP                  V3R lV 4       4      \        S4      4      x € K>  	  R# 5i)zSELECT {} FROM {} LIMIT 0ú, c              3   óV   <"  € T F  p\        S4       R \        V4       2x € K   	  R# 5i)Ú.N)rW   )r=   ÚcolrG   s   & €r0   r?   Ú9schema_read_probe_statements.<locals>.<genexpr>.<genexpr>´   s'   øé € Ð8bÑ]aÐVY¼B¸u»I¸;ÀaÌÈ3ËÀyÕ9QÓ]aùs   ƒ&)N)r/   r6   rW   )r=   ÚcolsrG   s   & @r0   r?   Ú/schema_read_probe_statements.<locals>.<genexpr>³   sC   øé € ð '
á5‘��tð (×.Ñ.¨t¯y©yÔ8bÑ]aÓ8bÓ/bÔdfÐglÓdm×nÐnÛ5ùs   ƒAA)Ú_READ_PROBE_STATEMENTSÚSessionSchemaMixinÚ_parse_schema_columnsr   rZ   ÚsortedÚitems)Útabless    r0   Úschema_read_probe_statementsrj   ©   sc   € ô Ò%Ü#×9Ñ9¼*ÓEˆß!&¤ñ '
ä% f§l¡l£nÔ5ó'
§¡ñ '
ä% f§l¡l£nÔ5ó'
ó "
Ðô "Ð!r2   c                   ó&  a € ] tR t^ºt o RtV 3R lR ltV 3R lR ltV 3R lR lt]V 3R lR	 l4       t	]V 3R
 lR l4       t
V 3R lR lt]V 3R lR l4       t]V 3R lR l4       t]V 3R lR l4       tV 3R lR lt]V 3R lR l4       tV 3R lR ltV 3R lR lt]RRRR/V 3R  lR! ll4       tV 3R" lR# ltV 3R$ lR% ltR&R'/V 3R( lR) lltV 3R* lR+ ltV 3R, lR- ltV 3R. lR/ lt]V 3R0 lR1 l4       tV 3R2 lR3 lt]V 3R4 lR5 l4       t]RPV 3R6 lR7 ll4       t]V 3R8 lR9 l4       tV 3R: lR; ltV 3R< lR= lt R> t!V 3R? lR@ lt"V 3RA lRB lt#V 3RC lRD lt$V 3RE lRF lt%V 3RG lRH lt&V 3RI lRJ lt'V 3RK lRL lt(V 3RM lRN lt)ROt*V t+R'# )Qre   u>   See module docstring â€” mixin for SessionDB (Schema cluster).c                ó8   <€ V ^8„  d   QhRS[ P                  RR/# ©r*   Úcursorr,   N©Úsqlite3ÚCursor)r/   Ú__classdict__s   "€r0   r1   ÚSessionSchemaMixin.__annotate__½   s   ø€ ÷ ñ ±G·N±Nð Àtñ r2   c                óR  €  VP                  R4      P                  4       pT F*  w  r4 T P	                  Y4      pTP                  RYS34       K,  	  R#   \        P                   d     R# i ; i  \        P                   d#   p\
        P                  RT4        Rp? R# Rp?ii ; i)a=  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Úfetchallrp   ÚOperationalErrorÚ_store_system_promptÚloggerÚwarning)Úselfrn   ÚrowsÚ
session_idÚpromptÚprompt_hashÚexcs   &&     r0   Ú_dedupe_legacy_system_promptsÚ0SessionSchemaMixin._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                ó:   <€ V ^8„  d   QhRS[ P                  RS[/# ©r*   rn   r,   ©rp   rq   rI   )r/   rr   s   "€r0   r1   rs   Õ   s   ø€ ÷ 	ñ 	©G¯N©Nð 	¹tñ 	r2   c                óä   €  VP                  R 4       VP                  R4       R#   \        P                   d5   pT P                  T4      '       g   h T P	                  T4        Rp?R# Rp?ii ; i)z:CREATE VIRTUAL TABLE temp._hermes_fts5_probe USING fts5(x)z"DROP TABLE temp._hermes_fts5_probeTNF)ru   rp   rw   Ú_is_fts5_unavailable_errorÚ_warn_fts5_unavailable)r{   rn   r€   s   && r0   Ú_sqlite_supports_fts5Ú(SessionSchemaMixin._sqlite_supports_fts5Õ   s_   € ð	Ø�N‰NÐWÔXØ�N‰NÐ?Ô@ÙøÜ×'Ñ'ô 	Ø×2Ñ2°3×7Ò7ØØ×'Ñ'¨Ô,Ýûð		ús   ‚"& ¦A/»)A*Á*A/c                ó8   <€ V ^8„  d   QhRS[ P                  RR/# rm   ro   )r/   rr   s   "€r0   r1   rs   à   s#   ø€ ÷ Dñ D©W¯^©^ð DÀñ Dr2   c                óü   € V P                  V4       \         FL  p\        P                  ! \        P
                  4      ;_uu_ 4        VP                  R V 24       RRR4       KN  	  R#   + '       g   i     Kc  ; i)úDROP TRIGGER IF EXISTS N)Ú_drop_fts_triggersr   Ú
contextlibÚsuppressrp   rw   ru   )r{   rn   Útriggers   && r0   Ú_drop_all_fts_triggersÚ)SessionSchemaMixin._drop_all_fts_triggersà   sW   € Ø×Ñ Ô'ß(ˆGÜ×$Ò$¤W×%=Ñ%=×>Õ>Ø—‘Ð!8¸¸	ÐBÔC÷ ?Ñ>ó )ß>×>Ð>ús   ÁA*Á*A;c                óP   <€ V ^8„  d   QhRS[ P                  RS[S[,          RS[/# )r*   rn   Únamesr,   )rp   rq   r   r.   rI   )r/   rr   s   "€r0   r1   rs   ç   s/   ø€ ÷ Qñ Q¡g§n¡nð Q¹XÁc½]ð QÉtñ Qr2   c                óÜ   € V'       g   R# RP                  R V 4       4      pRV R2p\        V P                  V\        V4      4      P	                  4       ^ ,          4      \        V4      8  # )z;True unless every trigger in *names* (one DDL half) exists.FÚ,c              3   ó&   "  € T F  pR x € K	  	  R# 5i©Ú?Nr;   ©r=   Ú_s   & r0   r?   Ú;SessionSchemaMixin._fts_triggers_missing.<locals>.<genexpr>ë   s   é € Ð3©U¨¥«Uùó   ‚zGSELECT COUNT(*) FROM sqlite_master WHERE type = 'trigger' AND name IN (Ú))r6   r-   ru   rZ   ÚfetchoneÚlen)rn   r•   ÚplaceholdersÚsqls   &&  r0   Ú_fts_triggers_missingÚ(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ÐPr2   c                ó6   <€ V ^8„  d   QhRS[ S[,          RS[/# )r*   r£   r,   )r   r.   rI   )r/   rr   s   "€r0   r1   rs   ð   s#   ø€ ÷ Sñ S±¹#µð SÁ4ñ Sr2   c                ó”   € V '       g   R# RP                  V P                  4       4      P                  4       pRV9  ;'       d    RV9   # )z?True when trigger SQL is a broad AFTER UPDATE (missing ``OF``).Fr4   zAFTER UPDATE OF zAFTER UPDATE ON )r6   ÚsplitÚupper)r£   Úcompacts   & r0   Ú#_fts_update_trigger_needs_narrowingÚ6SessionSchemaMixin._fts_update_trigger_needs_narrowingï   s@   € ÷ ÙØ—(‘(˜3Ÿ9™9›;Ó'×-Ñ-Ó/ˆØ!¨Ñ0×RÐRÐ5GÈ7Ñ5RÐRr2   c                ó:   <€ V ^8„  d   QhRS[ P                  RS[/# r„   )rp   rq   r-   )r/   rr   s   "€r0   r1   rs   ÷   s   ø€ ÷ *ñ *¹¿¹ð *ÉCñ *r2   c                ó®  € V P                  V4      pRV'       d   RMR,           pRP                  R V 4       4      pRV R2pVP                  WS4      P                  4       pV UUu. uF   w  ruV P	                  V4      '       g   K  VNK"  	  pppV'       g   ^ # V F  pVP                  RV 24       K  	  \
        V,          w  ršV P                  VRV	4       V P                  VRV
4       RV9   d    V P                  V4       TP                  R
R4      P                  4       pT'       d   T P	                  T^ ,          4      '       d'   T P                  T4       \        P                  R4       \        P                  R\        V4      4       \        V4      # u uppi   \         d)    T P                  T4       \        P                  R	4       h i ; i)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_cjk_updater]   c              3   ó&   "  € T F  pR x € K	  	  R# 5ir™   r;   r›   s   & r0   r?   ÚHSessionSchemaMixin._migrate_broad_fts_update_triggers.<locals>.<genexpr>  s   é € Ð ;©l¨¥«lùrž   zHSELECT name, sql FROM sqlite_master WHERE type = 'trigger' AND name IN (rŸ   r�   Úmessages_ftsÚmessages_fts_trigramz2CJK 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))Úmessages_fts_updateÚmessages_fts_trigram_updater;   )r¯   )Ú_db_has_legacy_inline_ftsr6   ru   rv   r«   Ú_FTS_DDLÚ_ensure_fts_schemaÚ_ensure_fts_cjk_schemaÚ	ExceptionÚ)_quarantine_cjk_after_update_of_migrationry   Ú	exceptionr    rz   Úinfor¡   )r{   rn   Úlegacy_layoutÚupdate_namesr¢   r£   r|   ÚnameÚto_dropÚbase_sqlÚtrigram_sqlÚrows   &&          r0   Ú"_migrate_broad_fts_update_triggersÚ5SessionSchemaMixin._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�4©ˆÑ_ßÙÛˆDØ�N‰NÐ4°T°FÐ;Ö<ñ ô !)¨Õ 7ÑˆØ×Ñ ¨¸ÔAØ×Ñ Ð(>ÀÔLð %¨Ô/ðØ×+Ñ+¨FÔ3ð
 —.‘.ØSÐUqóç‰h‹jð ÷ ˜$×BÑBÀ3ÀqÅ6×JÒJØ×>Ñ>¸vÔFÜ—‘ðHôô 	�‰ÐfÔhkÐlsÓhtÔuÜ�7‹|Ðùó; `øô ô Ø×>Ñ>¸vÔFÜ× Ñ Ð!UÔVØðús   Á'FÂFÃ4F! Æ!3Gc                ó:   <€ V ^8„  d   QhRS[ P                  RS[/# r„   r…   )r/   rr   s   "€r0   r1   rs   $  s#   ø€ ÷ 
Lñ 
L±·±ð 
LÁ4ñ 
Lr2   c                ó„   € V P                  R4      P                  4       pVRJ;'       d    RV^ ,          ;'       g    R9  # )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_srcrL   ©ru   r    )rn   rÄ   s   & r0   Ú_fts_index_is_misaligned_sourceÚ2SessionSchemaMixin._fts_index_is_misaligned_source#  sC   € ð �n‰nØZó
ç
‰(‹*ð 	ð ˜$ˆ×KÐKÐ#5¸cÀ!½f¿l¸lÈÑ#KÐKr2   c                ó>   <€ V ^8„  d   QhRS[ P                  RS[RR/# ©r*   rn   Úddlr,   N©rp   rq   r.   )r/   rr   s   "€r0   r1   rs   1  s,   ø€ ÷  Gñ  G±w·~±~ð  GÉCð  GÐTXñ  Gr2   c                óÜ  € \        V P                  R4      P                  4       4      pRpRpVP                  4        Fÿ  pWER,           ,          p\        P
                  ! V4      '       g   K0  RVP                  4       rdVP                  4       pVP                  R4      '       d   TpKl  VP                  R4      '       dT   VP                  RR^4      P                  R4      pVP                  VP                  R^4      ^,          4      V8X  d   RpKÖ  Ve   V P                  V4       RpV P                  V4       EK  	  VP                  4       '       d   \        P                  ! R	4      hR# )
uS  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'NrL   rC   r�   zCREATE TRIGGER IF NOT EXISTS zIF NOT EXISTS Ú;zincomplete DDL statement)Údictru   rv   Ú
splitlinesrp   Úcomplete_statementÚstripr©   Ú
startswithrV   ÚrstripÚgetr¨   rw   )	rn   rÏ   ÚstoredÚpending_dropÚ	statementÚlineÚcurrentr©   Údesireds	   &&       r0   Ú&_execute_ddl_skipping_settled_triggersÚ9SessionSchemaMixin._execute_ddl_skipping_settled_triggers0  s1  € ô �f—n‘nÐ%aÓb×kÑkÓmÓnˆØ&*ˆØˆ	Ø—N‘NÖ$ˆDØ �Õ$ˆIÜ×-Ò-¨i×8Ò8ÙØ!# Y§_¡_Ó%6�wØ—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ñ r2   c                ó>   <€ V ^8„  d   QhRS[ P                  RS[RR/# rÎ   rÐ   )r/   rr   s   "€r0   r1   rs   T  s+   ø€ ÷ 	Kñ 	K±'·.±.ð 	KÁsð 	KÈtñ 	Kr2   c                ó  € RpVP                  4        FC  pW#R,           ,          p\        P                  ! V4      '       g   K0  V P                  V4       RpKE  	  VP	                  4       '       d   \        P
                  ! R4      hR# )zAExecute a DDL script without ``executescript``'s implicit commit.rL   rC   zincomplete FTS DDL statementN)rÔ   rp   rÕ   ru   rÖ   rw   )rn   rÏ   rÜ   rÝ   s   &&  r0   Ú!_execute_ddl_script_transactionalÚ4SessionSchemaMixin._execute_ddl_script_transactionalS  sn   € ð ˆ	Ø—N‘NÖ$ˆDØ �Õ$ˆIÜ×)Ò)¨)×4Ô4Ø—‘˜yÔ)Ø’	ñ	 %ð
 �?‰?×ÒÜ×*Ò*Ð+IÓJÐJñ r2   c                ó>   <€ V ^8„  d   QhRS[ P                  RS[RR/# )r*   rn   Úlegacyr,   Nr…   )r/   rr   s   "€r0   r1   rs   _  s(   ø€ ÷ 4=ñ 4=±W·^±^ð 4=ÑPTð 4=ÐY]ñ 4=r2   c               óæ  a aaa€ V'       g   \        S4      '       g   R# \        S4      oS'       g   S P                  S4      '       g   R# SP                  R4      P	                  4       RJoR VVV V3R llpS'       g-   SP                  R4        V! 4        SP                  R4       R# S P                  SV4       R#   \
         d%    SP                  R4       SP                  R4       h i ; i)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), and so does a definition no release of this code created (a newer
release's, after a downgrade).NzSELECT 1 FROM messages LIMIT 1c                ó   € V ^8„  d   QhRR/# ©r*   r,   Nr;   )r/   s   "r0   r1   ÚGSessionSchemaMixin._migrate_misaligned_fts_source.<locals>.__annotate__p  s   € ÷ 	>ñ 	>˜$ñ 	>r2   c                  ó  <€ \          F  p SP                  R V  24       K  	  SP                  R4       S'       d   SP                  R4       SP                  SR\        4       S'       dq   SP                  R4       \	        SR4      eR   SP                  SR4      pV'       d   SP                  R4       M%Vf"   \         F  p SP                  R V  24       K  	  SP                  \        4       SP                  \        4       R# )	r�   z!DROP TABLE IF EXISTS messages_ftszDROP VIEW messages_fts_srcr²   z8INSERT INTO messages_fts(messages_fts) VALUES('rebuild')Úfts_rebuild_high_waterNr³   úHINSERT INTO messages_fts_trigram(messages_fts_trigram) VALUES('rebuild'))	Ú_FTS_BASE_TRIGGERSru   r¸   r   r!   Ú_fts_table_proberB   Ú_CLEAR_REBUILD_MARKERS_SQLÚ!_DROP_RETIRED_TOOL_HIGH_WATER_SQL)rÀ   Útrigramrn   Úhas_messagesr{   Ú
stale_views     €€€€r0   Údo_alignÚCSessionSchemaMixin._migrate_misaligned_fts_source.<locals>.do_alignp  sÒ   ø€ ß*�Ø—‘Ð!8¸¸Ð?Ö@ñ +à�N‰NÐ>Ô?ßà—‘Ð;Ô<Ø×#Ñ# F¨N¼GÔDßØ—‘ÐYÔZô ˜VÐ%=Ó>ÒJØ"×3Ñ3°FÐ<RÓS�GßØŸ™Ð'qÕrØ š÷ %:˜DØ"ŸN™NÐ-DÀTÀFÐ+KÖLñ %:à�N‰NÔ5Ô6Ø�N‰NÔ<Ö=r2   zSAVEPOINT fts_align_emptyz!RELEASE SAVEPOINT fts_align_emptyz%ROLLBACK TO SAVEPOINT fts_align_empty)r   r   rË   ru   r    ÚBaseExceptionÚ_run_admitted_startup_rebuild)r{   rn   rç   rö   rô   rõ   s   ff$ @@r0   Ú_migrate_misaligned_fts_sourceÚ1SessionSchemaMixin._migrate_misaligned_fts_source_  sÊ   û€ ÷ Ô.¨v×6Ò6ÙÜ.¨vÓ6ˆ
ß $×"FÑ"FÀv×"NÒ"NÙØ—~‘~Ð&FÓG×PÑPÓRÐZ^Ð^ˆ÷	>ó 	>÷0 à�N‰NÐ6Ô7ðÙ”
Ø—‘ÐBÔCñ
 Ø×*Ñ*¨6°8Ö<øô !ô Ø—‘ÐFÔGØ—‘ÐBÔCØðús   ÂC Ã/C0c                ó@   <€ V ^8„  d   QhRS[ P                  RS[RS[/# )r*   rn   rÀ   r,   )rp   rq   r.   rI   )r/   rr   s   "€r0   r1   rs   –  s&   ø€ ÷ !ñ !¡W§^¡^ð !¹3ð !Á4ñ !r2   c                óH   € V P                  R V34      P                  4       RJ# )z=SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?NrÊ   )rn   rÀ   s   &&r0   Ú_sqlite_table_existsÚ'SessionSchemaMixin._sqlite_table_exists•  s(   € à�~‰~ØKÈdÈWó
ç
‰(‹*˜Dð!ð 	!r2   c                ó:   <€ V ^8„  d   QhRS[ P                  RS[/# r„   r…   )r/   rr   s   "€r0   r1   rs   ›  s   ø€ ÷ ñ ±g·n±nð Éñ r2   c                ón  a€ V P                  S4      '       g   V P                  S4      '       d   R# V P                  SR4      pVRJd   VRJ # \         F  pSP	                  RV 24       K  	  SP	                  R4       V P                  SR\        4      '       g   R# V P                  SV3R l4       R# )ay  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³   Fr�   z,DROP VIEW IF EXISTS messages_fts_trigram_srcc                  ó&   <€ S P                  R 4      # )rî   ©ru   ©rn   s   €r0   Ú<lambda>ÚDSessionSchemaMixin._migrate_trigram_cron_exclusion.<locals>.<lambda>³  s   ø€ �F—N‘NÐ#mÔnr2   )r¶   Ú%_db_has_trigram_tool_calls_projectionrð   rB   ru   r¸   r   rù   )r{   rn   Útrigram_existsrÀ   s   &f  r0   Ú_migrate_trigram_cron_exclusionÚ2SessionSchemaMixin._migrate_trigram_cron_exclusion›  s±   ø€ ð ×)Ñ)¨&×1Ò1°T×5_Ñ5_Ð`f×5gÒ5gÙØ×.Ñ.¨vÐ7MÓNˆØ Ó%ð " UÐ*Ð*ß)ˆDØ�N‰NÐ4°T°FÐ;Ö<ñ *à�‰ÐEÔFØ×&Ñ& vÐ/EÄ×WÒWÙð 	×*Ñ*ØÜnô	
ñ r2   c                ó8   <€ V ^8„  d   QhRS[ P                  RR/# rm   ro   )r/   rr   s   "€r0   r1   rs   ·  s$   ø€ ÷ gñ gÁÇÁð gÐSWñ gr2   c                ó  € RV n          V P                  \        RVR7        VP                  R4       R	#   \         d    \        P                  RRR7        L8i ; i  \         d    \        P                  RRR7        R	# i ; i)
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º   ry   Údebugru   ©r{   rn   s   &&r0   r»   Ú<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    ‰5 ¢A µ"AÁAÁ"BÂBrç   FÚinclude_trigramTc                óD   <€ V ^8„  d   QhRS[ P                  RS[RS[RR/# )r*   rn   rç   r  r,   Nr…   )r/   rr   s   "€r0   r1   rs   Æ  s/   ø€ ÷ 7ñ 7¡W§^¡^ð 7Áð 7Ñ_cð 7Ðosñ 7r2   c               ó  € V'       d   RMR	pV FU  pV'       d3   V P                  RV 24       V P                  RV R\         R24       K=  V P                  RV RV R24       KW  	  V'       g   V P                  \        4       R# R# )
a:  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.rK   rM   z(rowid, content) SELECT id, zFROM messagesÚ(z) VALUES('rebuild')N)r²   r³   )r²   )ru   Ú_LEGACY_INLINE_CONCAT_SQLrñ   )rn   rç   r  ri   Útbls   &$$  r0   Ú_rebuild_fts_indexesÚ'SessionSchemaMixin._rebuild_fts_indexesÅ  s‚   € ÷ >MÑ9ÐRcˆÛˆCßØ—‘ ¨c¨UÐ3Ô4Ø—‘ ¨c¨UÐ2NÔOhÐNiÐivÐwÖxà—‘ ¨c¨U°!°C°5Ð8KÐLÖMñ ÷ Ø�N‰NÔ5Ö6ñ r2   c                óP   <€ V ^8„  d   QhRS[ P                  RS[RS[S[,          /# )r*   rn   Ú
table_namer,   )rp   rq   r.   r   rI   )r/   rr   s   "€r0   r1   rs   Õ  s+   ø€ ÷ ñ ¡w§~¡~ð Á3ð É8ÑTXÍ>ñ r2   c                ó  €  VP                  RV R24       R#   \         d   pTp Rp?MÁRp?i\        P                   d§   pT P	                  T4      '       dA   T P                  T4      '       d   T P                  T4       MT P                  T4        Rp?R# R\        T4      P                  4       9   d    Rp?R# R\        T4      P                  4       9  d   h Tp Rp?MRp?ii ; i\        P                  RY$4       R# )	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)ru   ÚUnicodeDecodeErrorrp   rw   r‡   Ú_is_trigram_unavailable_errorÚ_warn_trigram_unavailablerˆ   r.   Úlowerry   rz   )r{   rn   r  r€   Ú
decode_excs   &&&  r0   rð   Ú#SessionSchemaMixin._fts_table_probeÕ  sÓ   € ð	Ø�N‰N˜^¨J¨<°xÐ@ÔAÙøÜ!ô 	Ø�JûÜ×'Ñ'ô 	Ø×.Ñ.¨s×3Ò3ð ×5Ñ5°c×:Ò:Ø×2Ñ2°3Õ7à×/Ñ/°Ô4ÝØ¤# c£(§.¡.Ó"2Ô2ÝØ ¬¨C«¯©Ó(8Ô8ØØ�Jûð	úô 	�‰ðLØMWô	
ñ s6   ‚ ™C)¤+«C)ÁC)Á-C$Á1#C$ÂC$Â>!C$Ã$C)c                ó:   <€ V ^8„  d   QhRS[ P                  RS[/# r„   r…   )r/   rr   s   "€r0   r1   rs   ö  s$   ø€ ÷ Lñ L±7·>±>ð LÑW[ñ Lr2   c                óV  € \         P                   ! 4       p VP                  R\        34      P                  4       pV'       d   \        V^ ,          4      MRp\        V\        4      '       d   TM/ p \        VP                  RV4      4      p\        VP                  R^ 4      4      ^,           p\        VP                  RV4      4      p	\        VP                  R^ 4      4      ^,           p
Ws8”  g   V^ 8  d   Tp\        V UUu0 uF  w  r¼V^ 8”  g   K  VkK  	  upp4      pWÖP                  R4      8w  d   T^r©V\        8¼  d   W7,
          \         8¼  dn   V P#                  V\         R7      pV'       dO   \$        P'                  R	Wè4       V P)                  4       p\        V UUu0 uF  w  r¼V^ 8”  g   K  VkK  	  upp4      p\+        V4      ;'       d!    V
\,        8¬  ;'       d    W9,
          \.        8¬  pRVR
VRVRVRV	RV
RV/pVP                  R\        \0        P2                  ! VRR7      34       V'       g
   RV n        R# WÐn        V'       dB   \$        P'                  RW£V	,
          R,          RP7                  R V 4       4      \8        4       M2V\        8¼  d(   W7,
          \         8¼  d   \$        P'                  RW‚4       \$        P;                  RW(4       R#   \        P
                   d    Rp EL¾i ; i  \        \        3 d    T^T^3w  rxrš EL.i ; iu uppi u uppi )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 1NÚ
first_seenÚattemptsÚ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.Ú	last_seenÚfutilerD   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%   r]   c              3   óD   "  € T F  pR V R\        V4       2x € K  	  R# 5i)zpid z: N)r8   )r=   r+   s   & r0   r?   ÚBSessionSchemaMixin._defer_stale_fts_for_holders.<locals>.<genexpr>5  s#   é € ÐVÉ+À3˜D   R¬¸Ó(<Ð'=Õ>Ë+ù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).)Útimeru   r   r    rp   ÚErrorr	   Ú
isinstancerÓ   ÚfloatrÙ   r-   Ú	TypeErrorÚ
ValueErrorrg   Ú_FTS_HOLDER_ESCALATE_ATTEMPTSÚ_FTS_HOLDER_ESCALATE_SECONDSÚ%_reap_inactive_orphan_desktop_holdersry   ÚerrorÚ_foreign_state_db_holdersrI   Ú_FTS_HOLDER_FUTILE_ATTEMPTSÚ_FTS_HOLDER_FUTILE_SECONDSÚjsonÚdumpsÚ_fts_deferred_holder_pidsr6   Ú_FTS_STALE_RETRY_SECONDSrz   )r{   rn   Úforeign_holdersÚnowrÄ   ÚparsedÚrecordr(  r)  r*  r+  r+   Ú_pathr,  Úreapedr/  Ú
diagnostics   &&&              r0   Ú_defer_stale_fts_for_holdersÚ/SessionSchemaMixin._defer_stale_fts_for_holdersö  s÷  € ô �iŠi‹kˆð	Ø—.‘.ØDÔG_ÐFaóç‰h‹jð ÷
 -0”  Q¥Ô(°TˆÜ% f¬d×3Ò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±O¡j cÀsÈQÁwŸc˜c±OÒOÓPˆØŸ*™* ]Ó3Ô3Ø.1°1Ð+ØÔ4Ô4¸Õ9IÔMiÔ9iØ×?Ñ?ØÔ1Mð @ó ˆF÷ Ü—‘ðNØOUôð #'×"@Ñ"@Ó"B�Ü$¹OÔ%W¹O©j¨cÈsÐUVÉw§c c¹OÒ%WÓX�Ü�kÓ"÷ 
ð 
ØÔ ;Ñ;×qÐqÀÕ@SÔWqÑ@qð 	ð ˜* k°3¸
ÀHÈmÐ]hØ˜]Ð,>Ð@PÐRZÐ\bð
ˆ
ð 	�‰ØuÜ%¤t§z¢z°*ÈÔ'MÐNô	
÷ Ø-1ˆDÔ*ÙØ)4Ô&ßÜ�L‰Lðoð !¨Õ#6¸$Õ">Ø—	‘	ÑVÉ+ÓVÓVÔXpõð Ô6Ô6¸3Õ;KÔOkÔ;kÜ�L‰LðZà[côô
 	�‰ðwàô	
ñ
 øô{ �}‰}ô 	Ø‹Cð	ûô œ:Ð&ô 	SØDGÈÈCÐQRÀNÑAˆJ -Ò1Að	Süó Pùó &Xs<   —%K  Á3A:K> ÄL
ÄL
Æ'L%
Æ7L%
Ë K;Ë:K;Ë>LÌLÚtimeout_secondsNc                ó@   <€ V ^8„  d   QhRS[ P                  RS[RS[/# ©r*   rn   rç   r,   r…   )r/   rr   s   "€r0   r1   rs   D  s,   ø€ ÷ Iñ I©¯©ð IÁDð IÑcgñ Ir2   c               óT  € V P                  4       pV'       d   V P                  W4      '       d   R# \        V P                  VR7      ;_uu_ 4       pV'       g!   \        P                  R4        RRR4       R# V P                  WR7      uuRRR4       #   + '       g   i     R# ; i)zð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.F)rM  z„Deferred stale state.db FTS rebuild: another process holds the rebuild authority; canonical writes and LIKE search remain available.N©rç   )r=  rK  r   Údb_pathry   rz   Ú_recover_stale_fts_locked)r{   rn   rç   rM  rD  Úadmitteds   &&$$  r0   Ú_recover_stale_ftsÚ%SessionSchemaMixin._recover_stale_ftsD  s‚   € ð ×8Ñ8Ó:ˆß˜t×@Ñ@À×YÒYÙÜ" 4§<¡<À×QÕQÐU]ßÜ—‘ðIôð ÷ RÑQð ×1Ñ1°&Ð1ÓH÷ R×Q×QÓQús   ÁBÁ:BÂB'	c                ó    <€ V ^8„  d   QhRS[ /# rY   )rI   )r/   rr   s   "€r0   r1   rs   T  s   ø€ ÷ ;ñ ;©Tñ ;r2   c           	     ó   € V P                   '       g   R# V P                  4       e   RV n        RV n        R# V P                  '       g   V P
                  f   R# \        P                  ! 4       p\        V RR4      pV\        V RR4      8  dI   Ve<   \        V P                  4        UUu0 uF  w  r4V^ 8”  g   K  VkK  	  upp4      V8X  d   R# RV n        \        \        V RR4      4      pVR8:  d   \        pW,           V n        \        \        V\        R4      R,          \        4      V n         V P                   ;_uu_ 4        V P
                  e   V P                   '       g    RRR4       R# V P
                  P#                  4       pV P%                  V4      pV P'                  WgRR	7      pV'       d   V P)                  V4       RV n        \*        P,                  ! \.        P0                  4      ;_uu_ 4        V P
                  P3                  4        RRR4       VuuRRR4       # u uppi   + '       g   i     L"; i  + '       g   i     R# ; i  \4         d    \6        P9                  R
RR7        R# i ; i)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        rB  Ú_fts_stale_retry_afterÚ_fts_stale_retry_intervalr(   g       @)rç   rM  zUIn-process retry of the deferred stale state.db FTS rebuild failed; will retry later.Tr  )Ú
_fts_staleÚ_quarantine_reasonrY  rZ  Ú	read_onlyÚ_connr3  Ú	monotonicÚgetattrrg   r=  r6  rC  ÚminÚmaxÚ_FTS_STALE_RETRY_MAX_SECONDSÚ_lockrn   r¶   rU  r¹   r�   r�   rp   r4  Úcommitrº   ry   rz   )	r{   rE  Údeferred_pidsr+   rH  Úintervalrn   rç   Ú	recovereds	   &        r0   Úretry_deferred_fts_recoveryÚ.SessionSchemaMixin.retry_deferred_fts_recoveryT  s  € ð ��ˆÙØ×"Ñ"Ó$Ò0ð +.ˆDÔ'Ø-0ˆDÔ*ÙØ�>�>ˆ>˜TŸZ™ZÒ/ÙÜ�nŠnÓˆÜ Ð&AÀ4ÓHˆØ”˜Ð7¸Ó=Ô=ð Ò$¬Ø'+×'EÑ'EÔ'GÔSÑ'G™˜È3ÐQRÉ7—�Ñ'GÒSó)àô)ñ Ø-0ˆDÔ*Üœ Ð'BÀCÓHÓIˆØ�sŒ?Ü/ˆHØ&)¥nˆÔ#Ü),Ü�Ô2°CÓ8¸3Õ>Ô@\ó*
ˆÔ&ð	Ø——“Ø—:‘:Ò%¨T¯_¯_¨_Ø ÷ ‘ð Ÿ™×*Ñ*Ó,�Ø×7Ñ7¸Ó?�Ø ×3Ñ3°FÐ[^Ð3Ó_�	ßð ×/Ñ/°Ô7Ø58�DÔ2Ü×(Ò(¬¯©×7Õ7Ø—J‘J×%Ñ%Ô'÷ 8à ÷ ’ùó T÷. 8×7ú÷ —�ûô ô 	Ü�N‰NØgÐrvð ô ò ð		úsg   Â)H*
Â9H*
Ä1I Å IÅ&IÅ'I Å1B
IÇ;H0	È	IÈ
I È0I È;IÉI	ÉI ÉI É"I=É<I=c                ó@   <€ V ^8„  d   QhRS[ P                  RS[RS[/# rO  r…   )r/   rr   s   "€r0   r1   rs   ‘  s'   ø€ ÷ /ñ /±·±ð /É4ð /ÑTXñ /r2   c               óæ  €  V P                  VR4      RJ pT;'       g!    V'       * ;'       d    V P                  V4      pRP                  R \         4       4      pV'       d
   VR,          pVR,          pV'       dM   \        V'       d   \        MR,           pV\        R^4      ,          pV'       d   V\        R^RR7      ,          pM[\        V'       d   \        MR,           pVR	,          pV'       d
   VR
,          pV\        R,           \        ,           R,           ,          pRV,           V,           R\         R\         R2,           p VP!                  V4       RT n        RT n        Y@n        \0        P;                  R4       R#   \        P                  \        3 d    Rp ELhi ; i  \        P                   d°   p\"        P$                  ! \        P&                  4      ;_uu_ 4        T P(                  P+                  4        RRR4       M  + '       g   i     M; iT P-                  T4       T P(                  P/                  4        \0        P3                  RT4        Rp?R# Rp?ii ; i)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³   TrL   c              3   ó.   "  € T F  pR V R2x € K  	  R# 5i)r�   rÒ   Nr;   )r=   r‘   s   & r0   r?   Ú?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²   )rE   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_availablerp   ÚDatabaseErrorr   r6   r   r   r   rQ   r   r   rñ   rò   r   r   Úexecutescriptr�   r�   r4  r^  Úrollbackr’   re  ry   r<  r[  Ú_fts_enabledÚ_trigram_availablerz   )	r{   rn   rç   Útrigram_presentr  Údrop_sqlÚrebuild_sqlÚrecovery_sqlr€   s	   &&$      r0   rS  Ú,SessionSchemaMixin._recover_stale_fts_locked‘  s  € ð	lØ"×3Ñ3°FÐ<RÓSÐW[Ð[ˆOð
 .×kÐk°f´*×2jÐ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Õ3Ø—
‘
×#Ñ#Ô%÷ 4×3×3Ð3úð ×'Ñ'¨Ô/Ø�J‰J×ÑÔÜ�L‰LðEØFIôõ ûð
	úsB   ‚F ÅF, ÆF)Æ(F)Æ,I0Ç,I+Ç-HÈ	I+ÈH!ÈA	I+É+I0c                ó:   <€ V ^8„  d   QhRS[ P                  RS[/# r„   r…   )r/   rr   s   "€r0   r1   rs   Â  s   ø€ ÷ ñ ±7·>±>ð Ádñ r2   c                óz  € RpVP                  RV 24        VP                  RV R24        TP                  RT 24       R#   \        P                   dI   pT P                  T4      '       g   h T P	                  T4        Rp?TP                  RT 24       R# Rp?ii ; i  TP                  RT 24       i ; i)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)ru   rp   rw   r!  r"  )r{   rn   Úprober€   s   &&  r0   ro  Ú/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ÂB$ ÂB!Â!B$ Â$B:c                óR   <€ V ^8„  d   QhRS[ RS[S[ S[S[ S[ 3,          3,          /# )r*   Ú
schema_sqlr,   )r.   rÓ   )r/   rr   s   "€r0   r1   rs   Ô  s-   ø€ ÷ .ñ .©#ð .±$±s¹DÁÁcÀ½NÐ7JÕ2Kñ .r2   c           
     óê  € Rp\         P                  ! V P                  R4      4      P                  4       p\        P
                  ! \        4      ;_uu_ 4        ^ RIHp V! 4       R,          R,          p\        P                  ! VP                  RR7      4      p\        V\        4      '       d(   VP                  R4      V8X  d   VP                  R4      MRp\        V\        4      '       dc   \        ;QJ d*    R	 VP!                  4        4       F  '       d   K   R
M	  RM! R	 VP!                  4        4       4      '       d   VuuRRR4       # RRR4       \"        P$                  ! R4      p VP'                  V 4       / pVP)                  R4      P+                  4        F•  w  p/ p	VP)                  RV R24      P+                  4       p
V
 Fa  w  r¼rÞppV'       d   V.M. pV'       d   V'       g   VP-                  R4       Ve   VP-                  RV 24       RP/                  V4      Wœ&   Kc  	  W—V&   K—  	  VP1                  4        VeÕ   \        P
                  ! \        4      ;_uu_ 4        VP2                  P5                  RRR7       \6        P8                  ! \;        VP2                  4      RR7      w  pp\<        P>                  ! VRRR7      ;_uu_ 4       p\        P@                  ! RVRV/V4       RRR4       \<        PB                  ! VV4       RRR4       V# V#   + '       g   i     ELÝ; i  TP1                  4        i ; i  + '       g   i     L\; i  + '       g   i     T# ; i)uC  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   Úcachezschema_columns.json©ÚencodingÚschema_hashri   c              3   óò   "  € T Fm  p\        V\        4      ;'       dQ    \        ;QJ d*    R  VP                  4        4       F  '       d   K   RM	  RM! R  VP                  4        4       4      x € Ko  	  R# 5i)c              3   óB   "  € T F  p\        V\        4      x € K  	  R # 5irA   )r5  r.   )r=   Úvs   & r0   r?   ÚESessionSchemaMixin._parse_schema_columns.<locals>.<genexpr>.<genexpr>â  s   é € Ð.YÉ=Àa¬z¸!¼S×/AÐ/AË=ùs   ‚FTN)r5  rÓ   ÚallÚvalues)r=   rb   s   & r0   r?   Ú;SessionSchemaMixin._parse_schema_columns.<locals>.<genexpr>á  sN   é € ð 0ÙfuÐ^b”
˜4¤Ó&×YÐY¯3«3Ñ.YÈ4Ï;É;Ì=Ó.Y¯3¯3ª3Ñ.YÈ4Ï;É;Ì=Ó.YÓ+YÔYÓfuùs   ‚A7žA7ªA7Á+A7FTz:memory:zNSELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'úPRAGMA table_info("ú")zNOT NULLzDEFAULT r4   )ÚparentsÚexist_okz.schema_columns.)ÚdirÚprefixÚw)"ÚhashlibÚsha256ÚencodeÚ	hexdigestr�   r�   rº   Úhermes_constantsr   r@  ÚloadsÚ	read_textr5  rÓ   rÙ   rŠ  r‹  rp   Úconnectrq  ru   rv   Úappendr6   ÚcloseÚparentÚmkdirÚtempfileÚmkstempr.   ÚosÚfdopenÚdumprV   )r  Ú
cache_pathr…  Ú_homeÚblobri   ÚrefÚtable_columnsr  rb   r½   Ú_cidÚcol_nameÚcol_typeÚnotnullÚdefaultÚpkÚpartsÚfdÚtmpÚfhs   &                    r0   rf   Ú(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Ò+AÀdÇhÁhÈ}ÓF]ÐalÔFl�T—X‘X˜hÔ'ÐrvˆFÜ˜&¤$×'Ò'¯C«Cñ 0Øfl×fsÑfsÔfuó0¯C¯CªCñ 0Øfl×fsÑfsÔfuó0÷ -ò -ð ÷ ,×+Ò+ô �oŠo˜jÓ)ˆð	Ø×Ñ˜jÔ)Ø79ˆMØŸ+™+Ø`óç‰h‹jò‘�ð (*�Ø—{‘{Ð%8¸¸¸RÐ#@ÓA×JÑJÓL�ÛFJÑB�D H°wÀç*2˜X™J¸�Eß§rØŸ™ ZÔ0ØÒ*ØŸ™ x°¨yÐ%9Ô:Ø%(§X¡X¨e£_�D“Nñ GKð &*˜cÓ"ñð �I‰IŒKàÒ!Ü×$Ò$¤Y×/Õ/Ø×!Ñ!×'Ñ'°¸tÐ'ÔDÜ"×*Ò*¬s°:×3DÑ3DÓ/EÐN`Ôa‘��CÜ—Y’Y˜r 3°×9Õ9¸RÜ—I’I˜}¨k¸8À]ÐSÐUWÔX÷ :ä—
’
˜3 
Ô+÷ 0ð Ðˆ}Ð÷O ,×+Ð+ûð< �I‰I�Kú÷ :×9ú÷ 0Ö/ð Ðús^   ÁB!L%Ã:L%Ä(L%ÅL%Å0A:L9 Ç+L9 Ç3AL9 É1A-M!ËM	Ë:M!Ì%L6	Ì9MÍMÍM!Í!M2	c                ó8   <€ V ^8„  d   QhRS[ P                  RR/# rm   ro   )r/   rr   s   "€r0   r1   rs     s   ø€ ÷ ñ ©¯©ð ¸Dñ r2   c           
     óÊ  € V P                  \        4      pVP                  4        F�  w  r4 VP                  RV R24      P	                  4       pT'       g   K3  T Uu0 uF  qf^,          kK  	  ppTP                  4        F1  w  r‰Y‡9   d   K   TP                  RT R\        T4       RT	 24       K3  	  K‘  	  R#   \
        P                   d     K­  i ; iu upi   \
        P                   dr   p
\        T
4      P                  4       pRT9   d   \        P                  RY8T
4        Rp
?
K®  \        T
4      '       d   h \        P                  R	Y8T
4        Rp
?
KÝ  Rp
?
ii ; i)
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).r�  rŽ  zALTER TABLE "z" ADD COLUMN r4   zduplicate columnzreconcile %s.%s: %sNz;reconcile %s.%s failed; store remains behind SCHEMA_SQL: %s)rf   r   rh   ru   rv   rp   rw   rW   r.   r#  ry   r  r"   rz   )r{   rn   Úexpectedr  Údeclared_colsr|   rÄ   Ú	live_colsr«  r¬  r€   Úmessages   &&          r0   Ú_reconcile_columnsÚ%SessionSchemaMixin._reconcile_columns  sE  € ð ×-Ñ-¬jÓ9ˆØ)1¯©Ö)9Ñ%ˆJðØ—~‘~Ð(;¸J¸<ÀrÐ&JÓK×TÑTÓV�÷ Ùá+/Ó0©4 C˜QŸ˜©4ˆIÐ0Ø&3×&9Ñ&9Ö&;Ñ"�ØÔ(ÙðØ—N‘N ]°:°,¸mÌBÈxËLÈ>ÐYZÐ[cÐZdÐ#eÖfó	 '<ó *:øô ×+Ñ+ô Úðüò
 1øô ×/Ñ/ô Ü! #›hŸn™nÓ.�GØ)¨WÔ4äŸ™Ð%:¸JÐRUÔVÝ Ü+¨C×0Ò0ð ä—N‘NØUÐWaÐmp÷ò ûðús5   ¬#B;ÁCÂ#CÂ;CÃCÃE"Ã17EÄ.)EÅE"c                ó`   <€ V ^8„  d   QhRS[ P                  RS[RS[S[S[,          ,          /# )r*   rn   rG   r,   )rp   rq   r.   r   Úlist)r/   rr   s   "€r0   r1   rs   &  s4   ø€ ÷ 
Uñ 
U¡§¡ð 
U¹ð 
UÁÉÉcÍÕ@Sñ 
Ur2   c                óø   €  V P                  RV R24      P                  4       pV'       g   R# \	        R V 4       R R7       Uu. uF  q3^,          NK  	  up#   \        P                   d    Rp LNi ; iu upi )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).r�  rŽ  Nc              3   óF   "  € T F  q^,          '       g   K  Vx € K  	  R# 5i)é   Nr;   )r=   Úrs   & r0   r?   Ú6SessionSchemaMixin._live_pk_columns.<locals>.<genexpr>0  s   é € Ð%>±¨A¸1¿¹§a¢a³ùs   ‚!—
!c                 ó   € V ^,          # )rÁ  r;   )rÂ  s   &r0   r  Ú5SessionSchemaMixin._live_pk_columns.<locals>.<lambda>0  s	   € ÈaÐPQÎdr2   )Úkey)ru   rv   rp   rw   rg   )rn   rG   r|   rÂ  s   &&  r0   Ú_live_pk_columnsÚ#SessionSchemaMixin._live_pk_columns%  sy   € ð	Ø—>‘>Ð$7¸°w¸bÐ"AÓB×KÑKÓMˆD÷ Ùä$Ñ%>±Ó%>ÁNÕSÓTÑS˜�!—�ÑSÑTÐTøô ×'Ñ'ô 	ØŠDð	üò
 Us   ‚#A ÁA7ÁA4Á3A4c                óP   <€ V ^8„  d   QhRS[ P                  RS[RS[RS[RS[RR/# )r*   rn   rG   Úlegacy_namerÏ   Úcopy_sqlr,   NrÐ   )r/   rr   s   "€r0   r1   rs   3  s>   ø€ ÷ !ñ !™wŸ~™~ð !±cð !Éð !ÑRUð !Ñadð !Ðuyñ !r2   c                ó*  € V P                   pVP                  '       d   \        P                  WW#WE4       R# V P	                  R4        \        P                  WW#WE4       T P	                  R4       R#   \
         d    T P	                  R4       h i ; i)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_transactionre   Ú_rebuild_table_statementsru   rø   )rn   rG   rÊ  rÏ   rË  ÚindexesÚconns   &&&&&& r0   Ú_rebuild_tableÚ!SessionSchemaMixin._rebuild_table2  s„   € ð × Ñ ˆØ××ÐÜ×8Ñ8¸ÈÐZbÔlÙØ�‰Ð(Ô)ð	Ü×8Ñ8¸ÈÐZbÔlð 	�‰�xÖ øô ô 	Ø�N‰N˜:Ô&Øð	ús   Á
A4 Á4Bc                ó   <€ V ^8„  d   QhRR/# rê   r;   )r/   rr   s   "€r0   r1   rs   K  s   ø€ ÷  ñ  ÐY]ñ  r2   c                óÔ   € V P                  R V RV 24       V P                  V4       V P                  V4       V P                  RV 24       V F  pV P                  V4       K  	  R# )zALTER TABLE z RENAME TO zDROP TABLE Nr  )rn   rG   rÊ  rÏ   rË  rÒ  r£   s   &&&&&& r0   rÑ  Ú,SessionSchemaMixin._rebuild_table_statementsJ  s\   € à�‰˜ e W¨K¸°}ÐEÔFØ�‰�sÔØ�‰�xÔ Ø�‰˜ [ MÐ2Ô3ÛˆCØ�N‰N˜3Öó r2   c                ó8   <€ V ^8„  d   QhRS[ P                  RR/# rm   ro   )r/   rr   s   "€r0   r1   rs   S  s   ø€ ÷ 
ñ 
©w¯~©~ð 
À$ñ 
r2   c                óž   € V P                  VR4      pVe
   VRR.8X  d   R# \        P                  RV4       V P                  VRRRR4       R# )	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Ç  ry   r½   rÔ  )r{   rn   Úpk_colss   && r0   Ú_heal_gateway_routing_pkÚ+SessionSchemaMixin._heal_gateway_routing_pkS  sa   € ð ×'Ñ'¨Ð0AÓBˆØŠ?˜g¨'°=Ð)AÔAÙÜ�‰ØkÐmtô	
ð 	×ÑØÐ%Ð'BððEö	
r2   c                ó8   <€ V ^8„  d   QhRS[ P                  RR/# rm   ro   )r/   rr   s   "€r0   r1   rs   r  s   ø€ ÷ 65ñ 65±7·>±>ð 65Àdñ 65r2   c                ó   € V P                  VR4      pVe   RV9   d   R# \        P                  R\        V4      4       VP	                  R4        V P                  VRR\        R\        4       VP	                  R	4       R#   \        P                   d!   p\        P                  RT4        Rp?LCRp?ii ; i  TP	                  R	4       i ; i)
uH  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Ç  ry   r½   rg   ru   rÔ  Ú_SESSION_MODEL_USAGE_HEAL_DDLÚ_SESSION_MODEL_USAGE_INDEX_SQLrp   rw   r  )r{   rn   rß  r€   s   &&  r0   Ú_heal_session_model_usage_pkÚ/SessionSchemaMixin._heal_session_model_usage_pkr  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*   ÁB ÂB7ÂB2Â-B: Â2B7Â7B: Â:Cc                óÐ  € \        RRR7       V P                  P                  4       pVP                  \        4       V P                  V4       V P                  V4       V P                  V4        VP                  R4       V P                  V\        4       \        P                   ! \        P                  4      ;_uu_ 4        VP                  R4      P#                  4       e   VP                  R4       RRR4       V P%                  V4      pVP                  R	\&        34      P#                  4       pVRJV n        V P(                  '       d   V P+                  V4       V'       g   V P-                  V4       VP                  R
4      P#                  4       pVf�   VP                  R\.        34       \0        P0                  P3                  \0        P4                  4      P7                  4       pVP9                  RR\;        \<        P>                  ! 4       4      3RV3\@        R3.4       MV PC                  W^ ,          V4       V PE                  V4       V'       d#   V PG                  V4       V PI                  V4       V P                  PK                  4        R#   \        P                   d"   p\        P                  RT4        Rp?ELJRp?ii ; i  + '       g   i     ELæ; i)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^  rn   rq  r   r»  rà  ré  ru   rp   rw   ry   r  rà   r
   r�   r�   r    r‰   r   r[  r’   rŽ   r   ÚdatetimerE  ÚUTCÚ	isoformatÚexecutemanyr.   ÚuuidÚuuid4Ú_MESSAGE_UID_BACKFILL_DONEÚ_run_data_migrationsÚ_ensure_unique_title_indexÚ	_init_ftsÚ_stamp_settled_fts_layoutre  )r{   rn   r€   Úfts5_availableÚ	stale_rowrÄ   Únow_isos   &      r0   Ú_init_schemaÚSessionSchemaMixin._init_schema¬  sJ  € ô 	  Ð-CÕDØ—‘×"Ñ"Ó$ˆØ×ÑœZÔ(ð 	×Ñ Ô'Ø×%Ñ% fÔ-ð 	×)Ñ)¨&Ô1ð	QØ�N‰Nðeôð 	×3Ñ3°FÔ<NÔOô × Ò ¤×!9Ñ!9×:Õ:Ø�~‰~ÐSÓT×]Ñ]Ó_ÒkØ—‘ÐTÔU÷ ;ð ×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¯L©LÓ9×CÑCÓEˆGØ×ÑØMØ%¤s¬4¯:ª:«<Ó'8Ð9Ð<RÐT[Ð;\Ü,¨cÐ2ð4õð ×%Ñ% f°!­f°nÔEà×'Ñ'¨Ô/ßØ�N‰N˜6Ô"Ø×*Ñ*¨6Ô2Ø�
‰
×ÑÖøôY ×'Ñ'ô 	QÜ�L‰LÐJÈC×PÒPûð	Qú÷ ;×:Ð:ús$   Á1J Ã3KÊKÊ0KËKËK%	c                ó8   <€ V ^8„  d   QhRS[ P                  RR/# rm   ro   )r/   rr   s   "€r0   r1   rs     s#   ø€ ÷ fñ f±·±ð fÀ4ñ fr2   c                óv  € V P                   '       gN   \        V4      '       d=   \        VR4      f/   V P                  V4      '       g   V P	                  V4      '       d   R# VP                  R\        \        4      34      P                  4       f(   VP                  \        R\        \        4      34       R# R# )a_  Write ``fts_storage_version`` once the stored layout is exactly current and nothing is in flight.

The only open-time writer of the stamp, run after _init_fts so a fresh store is stamped on creation
and a realigned base index is stamped only when the trigram also matches. A definition this code does
not recognise (a newer release's) is never stamped down to this release's version. An interrupted
optimize (markers, trash, an empty external index against non-empty messages) keeps the stamp
unwritten so the resume offer stays alive. Reads before writing: a settled store never takes the write
lock here.
rí   NzPSELECT 1 FROM state_meta WHERE key = 'fts_storage_version' AND value = ? LIMIT 1Úfts_storage_version)
r[  r   r!   Ú_has_fts_trashÚ'_fts_external_index_empty_with_messagesru   r.   r   r    Ú_STATE_META_UPSERT_SQLr  s   &&r0   rü  Ú,SessionSchemaMixin._stamp_settled_fts_layout  s–   € ð �O�OˆOÜ% f×-Ò-Ü˜Ð!9Ó:ÒFØ×"Ñ" 6×*Ò*Ø×;Ñ;¸F×CÒCáØ�>‰>Ø^ÜÔ$Ó%Ð'ó
÷ ‰(‹*òð �N‰NÔ1Ð4IÌ3ÔObÓKcÐ3dÖeñ	r2   c                óD   <€ V ^8„  d   QhRS[ P                  RS[RS[RR/# )r*   rn   Úcurrent_versionrý  r,   N)rp   rq   r-   rI   )r/   rr   s   "€r0   r1   rs     s;   ø€ ÷ WWñ WW©7¯>©>ð WWÉCð WWÑaeð WWÐjnñ WWr2   c                óp  € \        RRR7       V^8  dŒ   \        P                  ! \        P                  4      ;_uu_ 4        VP                  R\        RR4       R\        R4       24       VP                  R	\        RR4       R\        RR
4       R24       RRR4       V^8  d    V P                  V4       V^8  dK   \        P                  ! \        P                  4      ;_uu_ 4        VP                  \        4       RRR4       V^8  d   V P                  V4       V^8  d4   V'       d,   V P                  V4      '       d   V P                  RRVR7       V^8  d   V P!                  V4       V P#                  V4       RpV^8  d   V'       d   V P%                  V4      pV\&        8  d.   V'       d$   V'       d   VP                  R\&        34       R# R# R# R#   + '       g   i     ELR; i  \         d"   p\        P                  RT4        Rp?ELcRp?ii ; i  + '       g   i     EL+; i)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î  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)Nz)v18 gateway metadata backfill skipped: %sÚfts_optimize_availabler  r  Tz%UPDATE schema_version SET version = ?)r   r�   r�   rp   rw   ru   r   r   Ú-_backfill_gateway_metadata_from_sessions_jsonrº   ry   r  Ú!_SESSION_MODEL_USAGE_V20_SEED_SQLÚ _migrate_v22_session_model_usageÚ_db_needs_fts_storage_upgrader  r�   Ú_advance_message_uid_backfillr	  r   )r{   rn   r
  rý  r€   Úfts_migrations_completes   &&&&  r0   rù  Ú'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×%=Ñ%=×>Õ>Ø—‘Ô@ÔA÷ ?à˜RÔØ×1Ñ1°&Ô9ð ˜RÔ§N°t×7YÑ7YÐZ`×7aÒ7aØ�M‰MÐ2°CÀˆMÔGØ˜RÔà×.Ñ.¨vÔ6Ø×*Ñ*¨6Ô2Ø"&ÐØ˜RÔ§Nð '+×&JÑ&JÈ6Ó&RÐ#ð2 œ^Ô+×0GÏNØ�N‰NÐBÄ^ÐDUÖVñ M[Ñ0GÑ+÷S ?×>Ð>ûô0 ô OÜ—‘ÐHÈ#×NÒNûðOú÷ ?×>Ð>ús1   ÁAG!Â(G5 Ã,H$Ç!G2	Ç5H!È HÈH!È$H5	c                ó8   <€ V ^8„  d   QhRS[ P                  RR/# rm   ro   )r/   rr   s   "€r0   r1   rs   s  s   ø€ ÷ ;ñ ;±G·N±Nð ;Àtñ ;r2   c                óT  € \        V\        4      e   R# \        V\        4      pV'       d   \        V^ ,          4      M^ pVP	                  R4      P                  4       ^ ,          ;'       g    ^ p\        P                  ! 4       \        ,           pW48  dj   V\        ,           pVP	                  RW634       TpW48  g   K0  \        P                  ! 4       V8¼  g   KL  V P                  \        \        V4      VR7       R# V P                  \        RVR7       \        V\        4       R# )aˆ  Mint ``message_uid`` onto rows written before the column existed, one bounded slice per open.

A single full-table UPDATE held the write lock for 261 s on an 835k-row / 4.8 GB store, so every
sibling process timed out with "database is locked". Rowid-range chunks commit one at a time (the
writer connection is autocommit) and an open spends at most ``_MESSAGE_UID_BACKFILL_BUDGET_S``
after its first chunk, so a large store converges over later opens. Until then a legacy row
simply has no uid, which every reader already treats as "no identity yet".

The walk is keyed on ``rowid``, not ``id``: on the declared table ``id INTEGER PRIMARY KEY`` is the
rowid alias (same plan), but a store converted from JSONL still carries ``id TEXT`` with no PRIMARY
KEY (``_reconcile_columns`` adds columns, never rebuilds the key), where ``MAX(id)`` is a str and an
``id`` range compares lexicographically (#141467).NzSELECT MAX(rowid) FROM messageszsUPDATE messages SET message_uid = lower(hex(randomblob(16))) WHERE rowid > ? AND rowid <= ? AND message_uid IS NULLr  r  )r!   rø  Ú_MESSAGE_UID_BACKFILL_CURSORr-   ru   r    r3  r_  Ú_MESSAGE_UID_BACKFILL_BUDGET_SÚ_MESSAGE_UID_BACKFILL_CHUNKr  r.   r    )r{   rn   rÄ   Údone_throughÚhighÚdeadliner©   s   &&     r0   r  Ú0SessionSchemaMixin._advance_message_uid_backfills  sê   € ô �VÔ7Ó8ÒDÙÜ˜Ô <Ó=ˆß&)”s˜3˜q�6”{¨qˆØ�~‰~Ð?Ó@×IÑIÓKÈAÕN×SÐSÐRSˆÜ—>’>Ó#Ô&DÕDˆØÔ!Ø Ô#>Õ>ˆEØ�N‰NðIØKWÐJ_ôað !ˆLØÖ"¤t§~¢~Ó'7¸8Ö'CØ—‘Ô:¼CÀÓ<MÐV\�Ô]ÙØ�‰Ô0°#¸fˆÔEÜ�VÔ9Ö:r2   c                ó8   <€ V ^8„  d   QhRS[ P                  RR/# rm   ro   )r/   rr   s   "€r0   r1   rs   ’  s#   ø€ ÷ Mñ M±w·~±~ð MÈ$ñ Mr2   c                ó  €  VP                  R4      P                  4       ^ ,          pV'       d   R# V P                  VRR\        R\        4       R#   \
        P                   d"   p\        P                  RT4        Rp?R# Rp?ii ; i)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 > 0Nrä  Ú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)	ru   r    rÔ  Ú_SESSION_MODEL_USAGE_V22_DDLrè  rp   rw   ry   r  )r{   rn   Ú	legacy_pkr€   s   &&  r0   r  Ú3SessionSchemaMixin._migrate_v22_session_model_usage’  s�   € ð	Mð
 Ÿ™Ønóç‰h‹j˜õˆI÷ ÙØ×ÑØÐ-Ð/HÔJfð?ô /öøô" ×'Ñ'ô 	MÜ�L‰LÐFÈ×LÒLûð	Mús   ‚-A ²A ÁBÁ'BÂBc                ó8   <€ V ^8„  d   QhRS[ P                  RR/# rm   ro   )r/   rr   s   "€r0   r1   rs   ³  s   ø€ ÷ +ñ +±·±ð +ÀDñ +r2   c           	     óô  a €  VP                  \        4       R#   \        P                   Ed1    TP                  R4        TP                  R4      P	                  4       p/ p\        TT 3R lRR7       F+  pTP                  T^,          . 4      P                  T4       K-  	  S P                  S P                  ,          pTP                  4        EF  w  rg\        T4       Uu. uF$  pS P                  T^,          4      T8X  g   K"  TNK&  	  Mu upi p	pT	'       d
   T	^ ,          MT^ ,          p
T	R,           FC  vr¼\        TP                  T4      pTP                  RYÛ34       \        P!                  RYm4       KE  	  T FW  w  r¾ppYº^ ,          8w  g   K  S P                  T4      T8w  g   K.  TP                  R	T34       \        P!                  R
Yö4       KY  	  EK  	  TP                  \        4       TP                  R4        R#   \        P"                   d=    TP                  R4       TP                  R4       \        P%                  R4         R# i ; i\        P&                   d     R# i ; i)a  Unique title index. Older DBs may hold duplicate titles from before the constraint.
Per title a user-owned (user/NULL) row beats llm > derived (``_title_rank``). User rows are
never dropped: the oldest keeps the title and newer ones become ``Title #k`` in started_at
order, so the "#N"-preferring lookup still opens the newest (#126764). Among auto titles
only, the newest highest-ranked keeps it; lower-ranked auto titles are cleared. Each change
is logged. Must never abort opening the DB, so the repair is guarded.zSAVEPOINT title_repairz¢SELECT rowid, title, title_source, started_at FROM sessions WHERE title IN (SELECT title FROM sessions WHERE title IS NOT NULL GROUP BY title HAVING COUNT(*) > 1)c                 óV   <€ SP                  V ^,          4      V ^,          V ^ ,          3# )r*   )Ú_title_rank)rÂ  r{   s   &€r0   r  Ú?SessionSchemaMixin._ensure_unique_title_index.<locals>.<lambda>Æ  s'   ø€ °t×7GÑ7GÈÈ!ÍÓ7MÈqÐQRÍtÐUVÐWXÕUYÑ6Zr2   T)rÆ  Úreverse:é   NNz-UPDATE sessions SET title = ? WHERE rowid = ?z-Renamed duplicate user session title %r to %rz0UPDATE sessions SET title = NULL WHERE rowid = ?z%Cleared duplicate %s session title %rzRELEASE title_repairzROLLBACK TO title_repairzICould not repair duplicate session titles; unique title index not createdN)ru   Ú_TITLE_UNIQUE_INDEX_SQLrp   ÚIntegrityErrorrv   rg   Ú
setdefaultrœ  Ú_TITLE_SOURCE_RANKÚTITLE_SOURCE_USERrh   Úreversedr)  r#   rÏ  ry   rz   r4  r¼   rw   )r{   rn   r|   ÚgroupsrÄ   Ú	user_rankÚtitleÚgrouprÂ  ÚusersÚkeepÚrowidrœ   ÚrenamedÚ_titleÚsourceÚ_starteds   f&               r0   rú  Ú-SessionSchemaMixin._ensure_unique_title_index³  s  ø€ ð$	Ø�N‰NÔ2Ö3øÜ×%Ñ%õ  	nð �N‰NÐ3Ô4ðnØ—~‘~ðnó÷ ‘(“*ð ð +-�Ü! $Ô,ZÐdh×i�CØ×%Ñ% c¨!¥f¨bÓ1×8Ñ8¸Ö=ñ jà ×3Ñ3°D×4JÑ4JÕK�	Ø$*§L¡L§N‘L�Eô )1°¬Ó_© 1¸4×;KÑ;KÈAÈaÍDÓ;QÐU^Ñ;^ŸQ˜QªùÒ_�EÐ_ß',˜5 ž8°%¸µ(�DØ%*¨2§Y Y˜	˜Ü"7¸×8IÑ8IÈ5Ó"Q˜ØŸ™Ð'VÐY`ÐXhÔiÜŸ™Ð'VÐX]Ögñ &/ó <AÑ7˜ v¨xØ ¨¥GÖ+°×0@Ñ0@ÀÓ0HÈIÖ0UØ"ŸN™NÐ+]Ð`eÐ_gÔhÜ"ŸN™NÐ+RÐTZÖbô <Añ %3ð —‘Ô6Ô7Ø—‘Ð5×6øÜ—=‘=ô nØ—‘Ð9Ô:Ø—‘Ð5Ô6Ü× Ñ Ð!l×mÐmðnúô ×'Ñ'ô 	Úð	úsU   ƒ š'I7ÁB"HÃ$DÄDÄBHÆHÆ0AHÈAIÉI7ÉIÉI7É2I7É6I7c                ó8   <€ V ^8„  d   QhRS[ P                  RR/# rm   ro   )r/   rr   s   "€r0   r1   rs   à  s#   ø€ ÷ A<ñ A<¡§¡ð A<°4ñ A<r2   c                óÆ  a aaaaa	€ S P                  S4      o\        SR	4      pS P                  '       g   S P                  SSR7       S P                  '       dD   S P	                  SSR7      '       d   S P                  S4       EM5R;S n        ;S n        S n        EM\        S,          w  oo	S P                  S\        4      ;'       g    RV9   pS P                  S\        4      ;'       g    RV9   pR VVVV V	3R llpV'       d   S P                  SV4       MmS P                  SRS4      S n        S P                  '       dD   S P                  SRS	4      oSS n        S'       d"   V'       d   S P                  SVVV V3R l4       S P                  '       d"   S'       g   V'       g   S P                  S4       S P                  '       d   S P                  S4       R# R# )
aD  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³   rQ  Fc                ó   € V ^8„  d   QhRR/# rê   r;   )r/   s   "r0   r1   Ú2SessionSchemaMixin._init_fts.<locals>.__annotate__ÿ  s   € ÷ 	8ñ 	8¨ñ 	8r2   c                  ó  <€ SP                  SR S 4      Sn        SP                  '       g   R# SP                  SRS4      Sn        SP                  SSSP                  R7       S'       g   SP	                  S4       R# R# )r²   Nr³   ©rç   r  )r¸   rs  rt  r  r¹   )rÂ   rn   Ú
legacy_ftsr{   rÃ   s   €€€€€r0   Úensure_and_rebuildÚ8SessionSchemaMixin._init_fts.<locals>.ensure_and_rebuildÿ  s{   ø€ Ø$(×$;Ñ$;¸FÀNÐT\Ó$]�Ô!Ø×(×(Ð(ÙØ*.×*AÑ*AÀ&ÐJ`ÐbmÓ*n�Ô'Ø×)Ñ)Ø :¸t×?VÑ?Vð *ô ÷ "Ø×/Ñ/°Ö7ñ "r2   c                  ó,   <€ SP                  S SSR 7      # )rD  )r  )rn   rE  r{   Útrigram_enableds   €€€€r0   r  Ú.SessionSchemaMixin._init_fts.<locals>.<lambda>  s   ø€  D×$=Ñ$=Ø &¨zÈ?ð %>ô %r2   N)r²   r³   Úmessages_fts_cjk)r¶   r   r[  rú   rU  r¹   rs  rt  r  r·   r¤   rï   rB   rù   r¸   rÅ   )
r{   rn   Úorphan_repairedÚbase_triggers_missingÚtrigram_triggers_missingrF  rÂ   rE  rI  rÃ   s
   ff    @@@@r0   rû  ÚSessionSchemaMixin._init_ftsà  s¦  ý€ ð
 ×3Ñ3°FÓ;ˆ
ô 9ØÐPó
ˆð ��ˆØ×/Ñ/°¸zÐ/ÔJØ�?�?ˆ?Ø×&Ñ& v°jÐ&×AÒAà×+Ñ+¨FÖ3àX]Ð]�Ô!Ð] DÔ$;¸dÖ>Uä$,¨ZÕ$8Ñ!ˆH�kð ×*Ñ*¨6Ô3EÓF×kÐkÈ.Ð\kÑJkð "ð ×*Ñ*¨6Ô3HÓI×vÐ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Ö;ñ r2   c                ó   <€ V ^8„  d   QhRR/# rê   r;   )r/   rr   s   "€r0   r1   rs   #  s   ø€ ÷ Vñ VÀ4ñ Vr2   c                óh  € \        V P                  4      ;_uu_ 4       pV'       d   V! 4         RRR4       R#  RRR4       \        P                  R4       VP	                  \
        \        34       V P                  V4       RV n        R;V n	        ;V n
        V n        R#   + '       g   i     Lo; i)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   rR  ry   rz   ru   Ú_STALE_KEY_UPSERT_SQLr   r’   r[  rs  rt  r  )r{   rn   Ú
rebuild_fnrT  s   &&& r0   rù   Ú0SessionSchemaMixin._run_admitted_startup_rebuild#  sŽ   € ô # 4§<¡<×0Ô0°HßÙ”Ø÷ 1Ñ0Ø÷ 1ô 	�‰ðwô	
ð 	�‰Ô,¬}Ð.>Ô?Ø×#Ñ# FÔ+ØˆŒØPUÐUˆÔÐU˜DÔ3°dÖ6M÷ 1×0ús   žB!Â!B1	c                ó8   <€ V ^8„  d   QhRS[ P                  RR/# rm   ro   )r/   rr   s   "€r0   r1   rs   ;  s    ø€ ÷ %ñ %ÁGÇNÁNð %ÐW[ñ %r2   c                óö  € \        4       R,          R,          pVP                  4       '       g   R# \        VRRR7      ;_uu_ 4       p\        P                  ! V4      pRRR4       \        X\        4      '       g   R# VP                  4        EFR  w  rV\        V4      P                  R4      '       g   \        V\        4      '       g   K>  VP                  R4      pV'       g   KY  VP                  R	4      p\        V\        4      '       d   TMRp	TP                  R
VP                  R4      ;'       g    TV	e   V	P                  R4      MRVP                  R4      V	e   V	P                  R4      MRVP                  R4      V	e   \        P                  ! V4      MRVP                  R4      '       g   VP                  R4      '       d   ^M^ \        V4      34       EKU  	  R#   + '       g   i     EL’; i)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.jsonNrÂ  r�  rƒ  rœ   r}   Ú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_flushed)r   ÚexistsÚopenr@  Úloadr5  rÓ   rh   r.   r×   rÙ   ru   rA  )
r{   rn   Úsessions_fileÚfÚdatarÆ  Úentryr}   rW  Úorigin_dicts
   &&        r0   r  Ú@SessionSchemaMixin._backfill_gateway_metadata_from_sessions_json;  s}  € ô (Ó)¨JÕ6¸ÕHˆØ×#Ñ#×%Ò%ÙÜ�- ¨w×7Õ7¸1Ü—9’9˜Q“<ˆD÷ 8ä˜$¤×%Ò%ÙØŸ*™*Ÿ,‰JˆCÜ�3‹x×"Ñ" 3×'Ò'¬z¸%Ä×/FÒ/FÙØŸ™ <Ó0ˆJßÙØ—Y‘Y˜xÓ(ˆFÜ$.¨v´t×$<Ò$<™&À$ˆKØ�N‰Nð#ð —I‘I˜mÓ,×3Ð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Ò6¸%¿)¹)ÐDT×:UÒ:U‘AÐ[\Ô^aÐblÓ^mð	÷ó '÷	 8×7Ð7ús   ÁG'Ç'G8	)r  rB  rs  r[  rY  rZ  rt  )r;   ),Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__Ú__doc__r�   r‰   r’   Ústaticmethodr¤   r«   rÅ   rË   rà   rä   rú   rþ   r	  r»   r  rð   rK  rU  ri  rS  ro  rf   r»  rÇ  rÔ  rÑ  rà  ré  r   rü  rù  r  r  rú  rû  rù   r  Ú__static_attributes__Ú__classdictcell__)rr   s   @r0   re   re   º   s+  ø‡ € ÙH÷ð ÷0	ð 	÷Dð Dð ÷Qó ðQð ÷Só ðS÷*ð *ðX ÷
Ló ð
Lð ÷ Gó ð GðD ÷	Kó ð	K÷4=ð 4=ðl ÷!ó ð!÷
ð ÷8gð gð ð7Àuð 7Ðfj÷ 7ñ 7ó ð7÷ð ÷BLð Lð\IÐZ^÷ Iò I÷ ;ð ;÷z/ð /÷bð ð" ÷.ó ð.÷`ð ðB ÷
Uó ð
Uð ÷!ñ !ó ð!ð. ÷ ó ð ÷
ð 
÷>65ð 65òtT÷lfð f÷0WWð WW÷r;ð ;÷>Mð M÷B+ð +÷ZA<ð A<÷FVð V÷0%ö %r2   re   c                ó<   € V ^8„  d   QhR\         P                  RR/# rm   ro   )r/   s   "r0   r1   r1   c  s   € ÷ 
$ñ 
$¤'§.¡.ð 
$°Tñ 
$r2   c                ó°   € \         P                  \        4      pVP                  V 4       \        P	                  V \
        4       VP                  V 4       R# )a  The one additive catch-up order for raw ``state.db`` connections: columns (SCHEMA_SQL indexes may
name columns an old table lacks), then the canonical DDL statement by statement (no
``executescript`` implicit COMMIT, so callers keep their own transaction), then columns again
(a no-op for tables the script just created whole). Locking, snapshots and commit stay with the
callers. ``_reconcile_columns`` only touches the staticmethod ``_parse_schema_columns``, so a bare
instance is enough and the column reconciler keeps one implementation.N)ÚobjectÚ__new__re   r»  rä   r   )rn   Úshims   & r0   Úapply_declared_schemart  c  s?   € ô �>‰>Ô,Ó-€DØ×Ñ˜FÔ#Ü×8Ñ8¸ÄÔLØ×Ñ˜FÖ#r2   c                ó<   € V ^8„  d   QhR\         P                  RR/# )r*   rÓ  r,   N)rp   Ú
Connection)r/   s   "r0   r1   r1   p  s    € ÷ ñ ¤×!3Ñ!3ð ¸ñ r2   c                ó€   € \        V P                  4       4       V P                  '       d   V P                  4        R# R# )a-  Bring a raw ``state.db`` connection to the canonical SCHEMA_SQL shape and commit.

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) through :func:`apply_declared_schema`.
N)rt  rn   rÐ  re  )rÓ  s   &r0   Úreconcile_state_schemarx  p  s,   € ô ˜$Ÿ+™+›-Ô(Ø××ÐØ�‰Žñ r2   c                óV   € V ^8„  d   Qh/ ^ \         9   d   \        \        ,          ;R&   # )r*   rd   )Ú__conditional_annotations__r   rZ   )r/   s   "r0   r1   r1      s$   € ÷ ñ ÷x /Ò .œ¤�Ñ .òyr2   )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)éÿÿÿÿz#                                   z                                ))]rz  rk  r�   rò  r”  Úloggingr@  r¢  rp   r   r3  rö  Útypingr   r   r   r   r˜  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   r   r   r   Úhermes_state_holdersr   Úhermes_state_searchr    r!   Úhermes_state_errorsr"   Úhermes_state_titlesr#   Ú	getLoggerry   r9  r:  r>  r?  rC  rc  rø  r  r  r  r8   rd   rZ   rB   rï   r·   r  rè  rç  r6   rÔ   rÖ   r#  r  r-  rR  r  rñ   rò   rQ   rW   rj   re   rt  rx  r1   )Úlnrz  s   0@r0   Ú<module>rˆ     s  øðöó Û Û Û Û Û 	Û Û Û Û ß 1Ó 1õ -Ý ;Ý !÷÷ ÷ ÷ õ ÷2ó 2å 0ß 7Ý 4Ý 5ð 
×	Ò	˜>Ó	*€à !Ð Ø#Ð ð !Ð Ø#Ð ð  Ð Ø%Ð à3Ð Ø8Ð Ø"Ð Ø!$Ð õEð
 +/Ð Ó .÷
 ˜ÑK©ÓKŸ™ÑK©ÓKÓKÐ ß�UÑV¡mÓV—U‘UÑV¡mÓVÓVÐ ð �G˜_Ð-¨t°nÐF\Ð5]Ð^€àað ð"Ð ð!Ð ð,  $Ÿy™yØ"×-Ñ-Ó/°Õ2Ð3Ø'D×'OÑ'OÓ'QÐRSÐTVÑ'WÓXÑ'W €x�"—(‘(“*×ÐÑ'WÑXõYàÐõó Ð ð%HÐ !ð< mð ð pð ð nð ð vÐ ð %lÐ !ðÐQV÷ õ0õ
"÷"fñ fõR%
$÷ùòU( Ys   Å$ G
