
    epje                         d Z ddlZddlZddlZddlmZmZ ddlmZ ddl	m
Z
mZmZmZmZmZmZmZmZmZmZ  ej        d          Z G d d          ZdS )	u  Schema creation, column reconciliation, and FTS DDL management for SessionDB.

Mixin contract: this is a plain mixin class consumed by
``hermes_state.SessionDB``. It defines no ``__init__`` and no state of its
own; methods access the host's attributes (``self._conn``, ``self.db_path``,
``self._execute_write`` and other SessionDB methods) established by
``SessionDB.__init__``. It must never import hermes_state (cycle) — shared
module-level constants live in hermes_state_common.
    N)DictOptional)get_hermes_home)DEFERRED_INDEX_SQLFTS_CJK_STALE_KEYFTS_SQLFTS_STORAGE_VERSIONFTS_TRIGRAM_SQLLEGACY_FTS_SQLLEGACY_FTS_TRIGRAM_SQL
SCHEMA_SQLSCHEMA_VERSION_FTS_TRIGGERS_ephemeral_child_sqlhermes_statec            	       *   e Zd ZdZdej        ddfdZdej        defdZe	dej        de
fd            Ze	dee         defd	            Zdej        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ej        deddfd            Ze	dddej        deddfd            Zdej        dede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dej        ddfdZdej        ddfdZd Zdej        ddfdZdS )SessionSchemaMixinu>   See module docstring — mixin for SessionDB (Schema cluster).cursorreturnNc                    	 |                     d                                          }n# t          j        $ r Y dS w xY w|D ]}t	          |t          j                  r|d         n|d         }t	          |t          j                  r|d         n|d         }|                     ||          }|                     d||f           dS )zEMove inline prompt snapshots into the shared content-addressed table.zFSELECT id, system_prompt FROM sessions WHERE system_prompt IS NOT NULLNidr   system_prompt   zMUPDATE sessions SET system_prompt_hash = ?, system_prompt = NULL WHERE id = ?)executefetchallsqlite3OperationalError
isinstanceRow_store_system_prompt)selfr   rowsrow
session_idpromptprompt_hashs          9/home/thesage/.hermes/hermes-agent/hermes_state_schema.py_dedupe_legacy_system_promptsz0SessionSchemaMixin._dedupe_legacy_system_prompts'   s    	>>2  hjj D ' 	 	 	FF	  		 		C&0gk&B&BNTAJ-7W[-I-IUS))sSTvF33FFCCKNN j)	   			 		s   '* ==c                     	 |                     d           |                     d           dS # t          j        $ r6}|                     |          s |                     |           Y d }~dS 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   r   _is_fts5_unavailable_error_warn_fts5_unavailable)r!   r   excs      r'   _sqlite_supports_fts5z(SessionSchemaMixin._sqlite_supports_fts5<   s    	NNWXXXNN?@@@4' 	 	 	22377 '',,,55555		s   *. A3+A..A3c                    d                     d t          D                       }|                     d| dt                                                    }t	          t          |t          j                  s|d         n|d                   S )N,c              3      K   | ]}d V  dS ?N .0_s     r'   	<genexpr>z8SessionSchemaMixin._fts_trigger_count.<locals>.<genexpr>I   s"      ;;;;;;;;    zGSELECT COUNT(*) FROM sqlite_master WHERE type = 'trigger' AND name IN ()r   )joinr   r   fetchoneintr   r   r   )r   placeholdersr#   s      r'   _fts_trigger_countz%SessionSchemaMixin._fts_trigger_countG   s    xx;;];;;;;nnC3?C C C
 
 (**	 	
 C!=!=I3q663q6JJJr8   sqlc                     | sdS d                     |                                                                           }d|v rdS d|v S )z?True when trigger SQL is missing AFTER UPDATE OF (still broad).F zAFTER UPDATE OF zAFTER UPDATE ON )r:   splitupper)r?   compacts     r'   #_fts_update_trigger_needs_narrowingz6SessionSchemaMixin._fts_update_trigger_needs_narrowingR   sQ      	5((399;;''--//((5!W,,r8   c                    |                      |          }d}|st          | d          r|dz  }d                    d |D                       }|                    d| d|                                          }g }|D ]}t          |t          j                  s|d         n|d	         }t          |t          j                  s|d
         n|d         }	|                     |	          r|	                    |           |sdS |D ]}|                    d|            |r9| 
                    |dt                     | 
                    |dt                     n| 
                    |dt                     | 
                    |dt                     d|v r	 |                     |           n># t           $ r1 |                     |           t$                              d            w xY w|                     |          s/|                     |           t$                              d           t$                              dt/          |                     t/          |          S )a  Replace broad AFTER UPDATE FTS triggers with AFTER UPDATE OF variants.

        ``CREATE TRIGGER IF NOT EXISTS`` will not replace an existing broad
        trigger, so installs that already created ``AFTER UPDATE ON messages``
        would keep firing on every messages row touch (status/compaction
        writes included). Inspect ``sqlite_master``, drop any still-broad
        UPDATE triggers, and re-apply the current DDL constants.

        No FTS rebuild: content correctness was already gated by WHEN clauses
        on modern installs; OF only skips unnecessary trigger evaluation.

        Returns the number of triggers dropped (0 when already converged).
        )messages_fts_updatemessages_fts_trigram_update_ensure_fts_cjk_schemamessages_fts_cjk_updatez, c              3      K   | ]}d V  dS r1   r3   r4   s     r'   r7   zHSessionSchemaMixin._migrate_broad_fts_update_triggers.<locals>.<genexpr>w   s"       ; ; ; ; ; ; ; ;r8   zHSELECT name, sql FROM sqlite_master WHERE type = 'trigger' AND name IN (r9   r   namer   r?   zDROP TRIGGER IF EXISTS messages_ftsmessages_fts_trigramrK   z2CJK FTS re-ensure after UPDATE OF migration failedzeCJK 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_ftshasattrr:   r   r   r   r   r   rE   append_ensure_fts_schemar   r   r   r
   rI   	Exception)_quarantine_cjk_after_update_of_migrationlogger	exception_cjk_update_trigger_is_narrowedwarninginfolen)
r!   r   legacy_layoutupdate_namesr=   r"   to_dropr#   rM   r?   s
             r'   "_migrate_broad_fts_update_triggersz5SessionSchemaMixin._migrate_broad_fts_update_triggers_   s   " 66v>>
  	9/G!H!H 	988Lyy ; ;l ; ; ;;;~~C3?C C C
 
 (**	 	
  	% 	%C!+C!=!=N3q663v;D *3 < <L#a&&#e*C77<< %t$$$ 	1 	= 	=D NN;T;;<<<<  	##FNNKKK##.0F    ##FNGDDD##.   )G33//7777    BB6JJJ$$L    ;;FCC BB6JJJNNL  
 	$LL	
 	
 	

 7||s   F. .;G)c                     |                     dd                                          }|sdS t          |t          j                  s|d         n|d         }|                     |           S )z>True when messages_fts_cjk_update exists with AFTER UPDATE OF.zASELECT sql FROM sqlite_master WHERE type = 'trigger' AND name = ?rJ   Fr   r?   )r   r;   r   r   r   rE   )r!   r   r#   r?   s       r'   rX   z2SessionSchemaMixin._cjk_update_trigger_is_narrowed   ss    nn2(
 
 (**	 	
  	5&sGK88Hc!ffc%j;;C@@@@r8   c                 0   d| _         	 |                     t          d|           n,# t          $ r t                              dd           Y nw xY w	 |                    d           d	S # t          $ r  t                              dd           Y d	S w xY w)
a  Fail-closed after dropping CJK UPDATE during OF migration.

        Clears availability, persists ``fts_cjk_stale``, and drops any
        residual broad/partial CJK UPDATE trigger so a later open cannot
        ``CREATE TRIGGER IF NOT EXISTS`` a gap without rebuild.
        F1r   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   rT   rV   debugr   )r!   r   s     r'   rU   z<SessionSchemaMixin._quarantine_cjk_after_update_of_migration   s     #(	MM+SM@@@@ 	 	 	LL<      	
	NNKLLLLL 	 	 	LLM       	s!   ' &AAA+ +&BBTinclude_trigramri   c                    |                      d           |r|                      d           |                      d           d S )Nz8INSERT INTO messages_fts(messages_fts) VALUES('rebuild')zHINSERT INTO messages_fts_trigram(messages_fts_trigram) VALUES('rebuild')zVDELETE FROM state_meta WHERE key IN ('fts_rebuild_high_water', 'fts_rebuild_progress')r   r   ri   s     r'   _rebuild_fts_indexesz'SessionSchemaMixin._rebuild_fts_indexes   s`     	QRRR 	NNZ   	A	
 	
 	
 	
 	
r8   c                    |                      d           |                      d           |sdS |                      d           |                      d           dS )ag  Rebuild the LEGACY inline FTS indexes (pre-v23) from messages.

        Used only to repair a legacy DB whose triggers degraded under an
        earlier no-FTS5 runtime. Inline tables have no external-content
        'rebuild' source, so we DELETE + reinsert the concatenated content
        the legacy triggers produced. Never touches the v23 shape.
        zDELETE FROM messages_ftszINSERT INTO messages_fts(rowid, content) SELECT id, COALESCE(content, '') || ' ' || COALESCE(tool_name, '') || ' ' || COALESCE(tool_calls, '') FROM messagesNz DELETE FROM messages_fts_trigramzINSERT INTO messages_fts_trigram(rowid, content) SELECT id, COALESCE(content, '') || ' ' || COALESCE(tool_name, '') || ' ' || COALESCE(tool_calls, '') FROM messagesrk   rl   s     r'   _rebuild_legacy_fts_indexesz.SessionSchemaMixin._rebuild_legacy_fts_indexes   sy     	1222	
 	
 	
  	F9:::	
 	
 	
 	
 	
r8   
table_namec                 r   	 |                     d| d           dS # t          j        $ r}|                     |          rF|                     |          r|                     |           n|                     |           Y d }~d S dt          |                                          v rY d }~dS  d }~ww xY w)NzSELECT * FROM z LIMIT 0Tzno such tableF)	r   r   r   r*   _is_trigram_unavailable_error_warn_trigram_unavailabler+   strlower)r!   r   rp   r,   s       r'   _fts_table_probez#SessionSchemaMixin._fts_table_probe  s    	NN@J@@@AAA4' 	 	 	..s33  55c:: 52237777//444ttttt#c((.."2"222uuuuu	s!    B6AB1#B10B11B6
schema_sqlc                 r   t          j        d          }	 |                    |            i }|                    d                                          D ]\  }i }|                    d| d                                          D ]~}|d         }|d         pd}|d         }|d	         }	|d
         }
|r|gng }|r|
s|                    d           |	|                    d|	            d                    |          ||<   |||<   ||                                 S # |                                 w xY w)u  Extract expected columns per table from SCHEMA_SQL.

        Uses an in-memory SQLite database to parse the SQL — SQLite itself
        handles all syntax (DEFAULT expressions with commas, inline
        REFERENCES, CHECK constraints, etc.) so there are zero regex
        edge cases.  The in-memory DB is opened, the schema DDL is
        executed, and PRAGMA table_info extracts the column metadata.

        Adding a column to SCHEMA_SQL is all that's needed; the
        reconciliation loop picks it up automatically.
        z:memory:zNSELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'PRAGMA table_info("")r                zNOT NULLNzDEFAULT rA   )r   connectexecutescriptr   r   rR   r:   close)rw   reftable_columnstblcolsr#   col_namecol_typenotnulldefaultpkpartss               r'   _parse_schema_columnsz(SessionSchemaMixin._parse_schema_columns$  sg    oj))	j)))79M++B  hjj* * (*;;1#111 (**5 5C  #1vH"1v|H!!fG!!fGQB*2:XJJE 1r 1Z000*%9%9%9:::%(XXe__DNN%)c"" IIKKKKCIIKKKKs   C5D   D6c           
         |                      t                    }|                                D ])\  }}	 |                    d| d                                          }n# t
          j        $ r Y Dw xY wt                      }|D ]C}t          |t          t          f          r|d         n|d         }|                    |           D|                                D ]x\  }	}
|	|vro|	                    dd          }	 |                    d| d| d	|
            ?# t
          j        $ r'}t                              d
||	|           Y d}~pd}~ww xY wy+dS )uZ  Ensure live tables have every column declared in SCHEMA_SQL.

        Follows the Beets/sqlite-utils pattern: the CREATE TABLE definition
        in SCHEMA_SQL is the single source of truth for the desired schema.
        On every startup this method diffs the live columns (via PRAGMA
        table_info) against the declared columns, and ADDs any that are
        missing.

        This makes column additions a declarative operation — just add
        the column to SCHEMA_SQL and it appears on the next startup.
        Version-gated migration blocks are no longer needed for ADD COLUMN.
        ry   rz   r   rM   "z""zALTER TABLE "z" ADD COLUMN "z" zreconcile %s.%s: %sN)r   r   itemsr   r   r   r   setr   tuplelistaddreplacerV   rg   )r!   r   expectedrp   declared_colsr"   	live_colsr#   rM   r   r   	safe_namer,   s                r'   _reconcile_columnsz%SessionSchemaMixin._reconcile_columnsO  s    --j99)1)9)9 	 	%J~~8*888 (**  +   I $ $!+C%!?!?Ps1vvS[d####&3&9&9&;&;  "(9,, ( 0 0d ; ;I]J]]i]]S[]]    #3   
 1:x        -	 	s)   +A""A43A4?DE-EEc                    	 |                     d                                          }n# t          j        $ r Y dS w xY w|sdS d fdt	          fd|D             fd          D             }|dd	gk    rdS t
                              d
|           |                     d           |                     d           |                     d           |                     d           dS )u  Rebuild ``gateway_routing`` when its PRIMARY KEY predates scoping.

        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:

        * ``save_gateway_routing_entry`` fails with "ON CONFLICT clause does
          not match any PRIMARY KEY or UNIQUE constraint" (its upsert targets
          the composite key), and
        * ``replace_gateway_routing_entries`` fails with "UNIQUE constraint
          failed: gateway_routing.session_key" whenever the same session_key
          exists under a different scope — the exact isolation the composite
          key exists to provide.

        Each failed save logs a warning and falls back to sessions.json,
        so a legacy-shaped table produces endless per-save warning spam.
        Rebuild it once, preserving rows.  On a session_key collision across
        scopes (possible while the PK was wrong) the newest row wins.
        z$PRAGMA table_info("gateway_routing")Nc                 Z    t          | t          t          f          r| |         n| |         S Nr   r   r   r#   idxrM   s      r'   _colz9SessionSchemaMixin._heal_gateway_routing_pk.<locals>._col  '    )#t}==L3s883t9Lr8   c                 *    g | ]} |d d          S )r   rM   r3   r5   rr   s     r'   
<listcomp>z?SessionSchemaMixin._heal_gateway_routing_pk.<locals>.<listcomp>  s7     
 
 
 DAv
 
 
r8   c              3   6   K   | ]} |d d          |V  dS )r   r   Nr3   r   s     r'   r7   z>SessionSchemaMixin._heal_gateway_routing_pk.<locals>.<genexpr>  s6      55qDDAt$4$45555555r8   c                      | dd          S )Nr   r   r3   )r   r   s    r'   <lambda>z=SessionSchemaMixin._heal_gateway_routing_pk.<locals>.<lambda>  s    dd1a.. r8   )keyscopesession_keyz]gateway_routing has legacy primary key %r; rebuilding with composite (scope, session_key) keyz?ALTER TABLE gateway_routing RENAME TO gateway_routing_legacy_pkzCREATE 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)
)zINSERT 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 ASCz$DROP TABLE gateway_routing_legacy_pk)r   r   r   r   sortedrV   rZ   )r!   r   r"   pk_colsr   s       @r'   _heal_gateway_routing_pkz+SessionSchemaMixin._heal_gateway_routing_pk{  sr   .	>>6 hjj D ' 	 	 	FF	 	F	M 	M 	M
 
 
 
5555D555....  
 
 
 w...F1	
 	
 	

 	M	
 	
 	
 		
 	
 	
 	E	
 	
 	
 	=>>>>>s   '+ >>c                    	 |                     d                                          }n# t          j        $ r Y dS w xY w|sdS d fd|D             }d|v rdS t                              dt          |                     |                     d           	 |                     d           |                     d	           |                     d
           |                     d           |                     d           |                     d           n7# t          j        $ r%}t                              d|           Y d}~nd}~ww xY w|                     d           dS # |                     d           w xY w)u:  Rebuild ``session_model_usage`` when its PRIMARY KEY lacks ``task``.

        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)``.  ``_reconcile_columns()`` ADDs the ``task`` column
        as a bare nullable, but SQLite cannot ALTER a primary key, so the
        shipped composite 6-column key never lands.  The version-gated v22
        rebuild is unreachable on those installs (``current_version < 22``
        is already false), so every upsert in ``_record_model_usage()``
        fails with "ON CONFLICT clause does not match any PRIMARY KEY or
        UNIQUE constraint" — aborting the enclosing write transaction and
        silently zeroing all token *and* cost accounting (#73823).

        Idempotent; runs unconditionally on every open, same pattern as
        :meth:`_heal_gateway_routing_pk` above.  On healthy databases the
        PRAGMA check short-circuits and this is a no-op.
        z(PRAGMA table_info("session_model_usage")Nc                 Z    t          | t          t          f          r| |         n| |         S r   r   r   s      r'   r   z=SessionSchemaMixin._heal_session_model_usage_pk.<locals>._col  r   r8   c                 D    h | ]} |d d           |dd          S )r   r   r   rM   r3   r   s     r'   	<setcomp>zBSessionSchemaMixin._heal_session_model_usage_pk.<locals>.<setcomp>  sJ     
 
 
#$Q40@0@
DAv
 
 
r8   taskzdsession_model_usage has legacy primary key %r (missing task); rebuilding with composite 6-column keyzPRAGMA foreign_keys=OFFzGALTER TABLE session_model_usage RENAME TO session_model_usage_legacy_pka_  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, 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(DROP TABLE session_model_usage_legacy_pk]CREATE INDEX IF NOT EXISTS idx_session_model_usage_session ON session_model_usage(session_id)VCREATE INDEX IF NOT EXISTS idx_session_model_usage_model ON session_model_usage(model)z'session_model_usage PK heal skipped: %szPRAGMA foreign_keys=ON)r   r   r   r   rV   rZ   r   rg   )r!   r   r"   r   r,   r   s        @r'   _heal_session_model_usage_pkz/SessionSchemaMixin._heal_session_model_usage_pk  s   &	>>: hjj D ' 	 	 	FF	 	F	M 	M 	M
 
 
 
(,
 
 
 WF57OO	
 	
 	
 	0111?	5NN:   NN  6 NN9  & NNEFFFNN5   NN0    ' 	I 	I 	ILLBCHHHHHHHH	I NN344444FNN34444s=   '+ >>A>D E' E(EE' EE' 'E>c                 t   | j                                         }|                    t                     |                     |           |                     |           |                     |           	 |                    d           n7# t          j	        $ r%}t                              d|           Y d}~nd}~ww xY w|                    t                     	 |                    d           n# t          j	        $ r Y nw xY w|                     |          }d}|s|                     |           |                    d           |                                }||                    dt           f           nvt#          |t          j                  r|d         n|d	         }|d
k     rct           d
k    rX|rT|                     |d          }|du r5|                     |dt*                    r|                    d           n
d}n|d}nd}|dk     rt           dk     r	 |dk     rQ	 |                    dt-          d                      |                    d           n# t          j	        $ r Y nw xY w|dk     rI	 |                     |           n2# t0          $ r%}t                              d|           Y d}~nd}~ww xY w|dk     r,	 |                    d           n# t          j	        $ r Y nw xY w|dk     r	 |                    d                                          d	         }|s~|                    d           |                    d           |                    d           |                    d           |                    d           |                    d           n7# t          j	        $ r%}t                              d |           Y d}~nd}~ww xY w|dk     r/|r-|                     |          r|                     d!d"|#           |d$k     r|                     |           |r|                     |          s{|                    d%                                          T|                     |          s?|                     |          s*|                     d&t=          t>                    |#           |t           k     r |r|r|                    d't           f           d(}		 |                    |	           n# t          j         $ r~ 	 |                    d)           t          !                    d*|j"                   |                    |	           n/# t          j#        $ r t          $                    d+           Y nw xY wY nt          j	        $ r Y nw xY w|ro|                     |          r| %                    |          tM          tN                    k     }
|                     |d,tP                    | _)        | j)        r<|                     |dtT                    }|| _+        |
r| ,                    ||-           n| %                    |          tM          tN                    k     }
|                     |d,tZ                    | _)        | j)        rQ|                     |dt*                    }|| _+        |
r| .                    ||-           | /                    |           ta          | d.d          r| 1                    |           | j         2                                 dS )/a  Create tables and FTS if they don't exist, reconcile columns.

        Schema management follows the declarative reconciliation pattern
        (Beets, sqlite-utils): SCHEMA_SQL is the single source of truth.
        On existing databases, _reconcile_columns() diffs live columns
        against SCHEMA_SQL and ADDs any missing ones.  This eliminates
        the version-gated migration chain for column additions, making
        it impossible for reordered or inserted migrations to skip columns.

        The schema_version table is retained for future data migrations
        (transforming existing rows) which cannot be handled declaratively.
        zCREATE 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: %sNz3UPDATE messages SET active = 1 WHERE active IS NULLTz*SELECT version FROM schema_version LIMIT 1z/INSERT INTO schema_version (version) VALUES (?)versionr   
   rO   FzkINSERT INTO messages_fts_trigram(rowid, content) SELECT id, content FROM messages WHERE content IS NOT NULL         zUPDATE sessions SET model_config = json_set(COALESCE(model_config, '{}'), '$._delegate_from', parent_session_id) WHERE parent_session_id IS NOT NULL AND json_extract(COALESCE(model_config, '{}'), '$._delegate_from') IS NULL AND sessionsa  UPDATE sessions SET model_config = json_set(COALESCE(model_config, '{}'), '$._delegate_from', '__orphaned__') WHERE parent_session_id IS NULL AND json_extract(COALESCE(model_config, '{}'), '$._delegate_from') IS NULL AND json_extract(COALESCE(model_config, '{}'), '$._branched_from') 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)   z)v18 gateway metadata backfill skipped: %s   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) > 0   z\SELECT COUNT(*) FROM pragma_table_info('session_model_usage') WHERE name = 'task' AND pk > 0zAALTER TABLE session_model_usage RENAME TO session_model_usage_v21a  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 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"DROP TABLE session_model_usage_v21r   r   z+v22 session_model_usage rebuild skipped: %sfts_optimize_availablerb   rc      zESELECT 1 FROM state_meta WHERE key = 'fts_rebuild_high_water' LIMIT 1fts_storage_versionz%UPDATE schema_version SET version = ?zfCREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_title_unique ON sessions(title) WHERE title IS NOT NULLag  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 createdrN   rh   _fts_enabled)3_connr   r   r   r   r   r   r   r   r   rV   rg   r   r-   _drop_fts_triggersr;   r   r   r   rv   rS   r
   r   -_backfill_gateway_metadata_from_sessions_jsonrT   rP   rf   r(   _has_fts_trash'_fts_external_index_empty_with_messagesrt   r	   IntegrityErrorrY   rowcountErrorrW   r>   r[   r   r   r   r   _trigram_availablero   r   rm   rI   getattrr_   commit)r!   r   r,   fts5_availablefts_migrations_completer#   current_version_fts_trigram_exists	legacy_pktitle_index_sqltriggers_need_repairtrigram_enableds               r'   _init_schemazSessionSchemaMixin._init_schema<  s>	    ""$$Z((( 	'''
 	%%f--- 	))&111	QNN8   
 ' 	Q 	Q 	QLLJCPPPPPPPP	Q
 	/000	NNE    ' 	 	 	D	 33F;;"& 	, ##F+++
 	CDDDoo;NNA!   
 1;30L0LXc)nnRUVWRXO
 ##"(<(< " 4*.*?*? 6+ +' +e3322"$:O  < #NN!]   
 7<33,427/.3+##(;(; ##NNB  4J??	B B   NN
T    /   D##
SFFvNNNN  S S S
 LL!LcRRRRRRRRS ##!NNH   > /   D##7U &9! ! hjj!$I % 0'jkkk$  . ?   'KLLLA   <   / U U ULL!NPSTTTTTTTTU##: " Pd&D&DV&L&L PMM":CMOOO## 226::: 66v>> NNC  (**% ++F33	%
 DDVLL% )3/B+C+CF      .00+ 1" 1 ;#%  9 		NN?++++% 	 	 		 	 	 \O   ////=     5    
 ' 	 	 	D	  9	@ --f55 '8++F33c-6H6HH % %)$;$;NN% %! $ &*&=&= 68N' 'O />D++ 88"O 9   
 ++F33c-6H6HH % %)$;$;NG% %! $ 8&*&=&= 6' 'O />D++ 11",; 2    //777 t^U33 @77???
s   4B
 
B>B99B>C2 2DD*:I% %I76I7J 
K!KKK& &K87K8B-N0 0O$?OO$7T V*A
U('V*()VV*VV*V*)V*c                 ,   t                      dz  dz  }|                                sdS t          |dd          5 }t          j        |          }ddd           n# 1 swxY w Y   t          |t                    sdS |                                D ]~\  }}t          |          	                    d          st          |t                    s>|
                    d          }|sV|
                    d	          }|                    d
|
                    d          p|t          |t                    r|pi 
                    d          nd|
                    d          t          |t                    r|pi 
                    d          nd|
                    d          t          |t                    rt          j        |          nd|
                    d          s|
                    d          rdndt          |          f           dS )uv  One-time v18 backfill of gateway metadata from sessions.json.

        Existing gateway sessions predate the display_name / origin_json /
        expiry_finalized columns; copy what sessions.json knows so consumers
        can switch to state.db without losing pre-migration sessions.
        Only fills NULL columns — never overwrites data written by newer code.
        r   zsessions.jsonNr   zutf-8)encodingr6   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_flushedr   r   )r   existsopenjsonloadr   dictr   rt   
startswithgetr   dumps)	r!   r   sessions_filefdatar   entryr$   r   s	            r'   r   z@SessionSchemaMixin._backfill_gateway_metadata_from_sessions_json  sB    ())J6H##%% 	F-w777 	 19Q<<D	  	  	  	  	  	  	  	  	  	  	  	  	  	  	 $%% 	F**,, 	 	JC3xx""3'' z%/F/F <00J YYx((FNN# IIm,,35?5M5MWV\r&&y111SWIIk**7A&$7O7OYV\r&&{333UYIIn--*4VT*B*BLDJv&&&#566\%))DT:U:U\AA[\
OO	   	 	s   AA"%A")__name__
__module____qualname____doc__r   Cursorr(   boolr-   staticmethodr<   r>   r   rt   rE   r_   rX   rU   rm   ro   rv   r   r   r   r   r   r   r   r3   r8   r'   r   r   $   s       HHGN t    *	GN 	t 	 	 	 	 K7> Kc K K K \K 
-# 
-4 
- 
- 
- \
-S SC S S S Sj
Agn 
A 
A 
A 
A 
An	   4  !%
 
 

 
 
	
 
 
 \
,  !%
 
 

 
 
	
 
 
 \
Bw~ 3 8TX>    " (# ($sDcN7J2K ( ( ( \(T* *D * * * *XG?w~ G?$ G? G? G? G?Rv57> v5d v5 v5 v5 v5pJ J JX/n/	/ / / / / /r8   r   )r   loggingr   r   typingr   r   hermes_constantsr   hermes_state_commonr   r   r   r	   r
   r   r   r   r   r   r   	getLoggerrV   r   r3   r8   r'   <module>r      s(       ! ! ! ! ! ! ! ! , , , , , ,                           
	>	*	*S S S S S S S S S Sr8   