§
    ‰eîi ã                   óð   — d Z ddlZddlZddlZddlZddlZddlZddlZddlm	Z	 ddl
mZ ddlmZmZmZmZmZmZ  ej        e¦  «        Z ed¦  «        Z e¦   «         dz  ZdZd	Zd
Z G d„ d¦  «        ZdS )a}  
SQLite State Store for Hermes Agent.

Provides persistent session storage with FTS5 full-text search, replacing
the per-session JSONL file approach. Stores session metadata, full message
history, and model configuration for CLI and gateway sessions.

Key design decisions:
- WAL mode for concurrent readers + one writer (gateway multi-platform)
- FTS5 virtual table for fast text search across all session messages
- Compression-triggered session splitting via parent_session_id chains
- Batch runner and RL trajectories are NOT stored here (separate systems)
- Session source tagging ('cli', 'telegram', 'discord', etc.) for filtering
é    N)ÚPath)Úget_hermes_home)ÚAnyÚCallableÚDictÚListÚOptionalÚTypeVarÚTzstate.dbé	   aÿ  
CREATE TABLE IF NOT EXISTS schema_version (
    version INTEGER NOT NULL
);

CREATE TABLE IF NOT EXISTS sessions (
    id TEXT PRIMARY KEY,
    source TEXT NOT NULL,
    user_id TEXT,
    model TEXT,
    model_config TEXT,
    system_prompt TEXT,
    parent_session_id TEXT,
    started_at REAL NOT NULL,
    ended_at REAL,
    end_reason TEXT,
    message_count INTEGER DEFAULT 0,
    tool_call_count INTEGER DEFAULT 0,
    input_tokens INTEGER DEFAULT 0,
    output_tokens INTEGER DEFAULT 0,
    cache_read_tokens INTEGER DEFAULT 0,
    cache_write_tokens INTEGER DEFAULT 0,
    reasoning_tokens INTEGER DEFAULT 0,
    billing_provider TEXT,
    billing_base_url TEXT,
    billing_mode TEXT,
    estimated_cost_usd REAL,
    actual_cost_usd REAL,
    cost_status TEXT,
    cost_source TEXT,
    pricing_version TEXT,
    title TEXT,
    api_call_count INTEGER DEFAULT 0,
    FOREIGN KEY (parent_session_id) REFERENCES sessions(id)
);

CREATE TABLE IF NOT EXISTS messages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT NOT NULL REFERENCES sessions(id),
    role TEXT NOT NULL,
    content TEXT,
    tool_call_id TEXT,
    tool_calls TEXT,
    tool_name TEXT,
    timestamp REAL NOT NULL,
    token_count INTEGER,
    finish_reason TEXT,
    reasoning TEXT,
    reasoning_content TEXT,
    reasoning_details TEXT,
    codex_reasoning_items TEXT,
    codex_message_items TEXT
);

CREATE TABLE IF NOT EXISTS state_meta (
    key TEXT PRIMARY KEY,
    value TEXT
);

CREATE INDEX IF NOT EXISTS idx_sessions_source ON sessions(source);
CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id);
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);
aÔ  
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
    content,
    content=messages,
    content_rowid=id
);

CREATE TRIGGER IF NOT EXISTS messages_fts_insert AFTER INSERT ON messages BEGIN
    INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
END;

CREATE TRIGGER IF NOT EXISTS messages_fts_delete AFTER DELETE ON messages BEGIN
    INSERT INTO messages_fts(messages_fts, rowid, content) VALUES('delete', old.id, old.content);
END;

CREATE TRIGGER IF NOT EXISTS messages_fts_update AFTER UPDATE ON messages BEGIN
    INSERT INTO messages_fts(messages_fts, rowid, content) VALUES('delete', old.id, old.content);
    INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
END;
c            %       óR  — e Zd ZdZdZdZdZdZdpdefd„Z	d	e
ej        gef         d
efd„Zdqd„Zd„ Zd„ Z	 	 	 	 	 drdedededeeef         dededed
efd„Zdeded
dfd„Zded
dfd„Zdeded
dfd„Z	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dsdededededed ed!ed"ee         d#ee         d$ee         d%ee         d&ee         d'ee         d(ee         d)ee         d*ed+ed
df$d,„Z	 	 dtdededed
dfd.„Zded
eeeef                  fd/„Zd0ed
ee         fd1„Z d2Z!e"d3ee         d
ee         fd4„¦   «         Z#ded3ed
efd5„Z$ded
ee         fd6„Z%d3ed
eeeef                  fd7„Z&d3ed
ee         fd8„Z'd9ed
efd:„Z(ded
ee         fd;„Z)	 	 	 	 	 	 duded>e*e         d?ed@edAedBed
e*eeef                  fdC„Z+ded
eeeef                  fdD„Z,	 	 	 	 	 	 	 	 	 	 	 dvdedEedFedGedHedIedJedKedLedMedNedOedPed
efdQ„Z-ded
e*eeef                  fdR„Z.ded
efdS„Z/ded
e*eeef                  fdT„Z0e"dUed
efdV„¦   «         Z1e"dWed
efdX„¦   «         Z2	 	 	 	 	 dwdUedYe*e         d>e*e         dZe*e         d?ed@ed
e*eeef                  fd[„Z3	 	 	 dxded?ed@ed
e*eeef                  fd\„Z4dpded
efd]„Z5dpded
efd^„Z6ded
eeeef                  fd_„Z7dpded
e*eeef                  fd`„Z8ded
dfda„Z9ded
efdb„Z:dyddeded
efde„Z;dfed
ee         fdg„Z<dfedhed
dfdi„Z=dqdj„Z>	 	 	 dzdledmedned
eeef         fdo„Z?dS ){Ú	SessionDBzÆ
    SQLite-backed session storage with FTS5 search.

    Thread-safe for the common gateway pattern (multiple reader threads,
    single writer via WAL mode). Each method opens its own cursor.
    é   g{®Gáz”?g333333Ã?é2   NÚdb_pathc                 ó¼  — |pt           | _        | j        j                             dd¬¦  «         t	          j        ¦   «         | _        d| _        t          j	        t          | j        ¦  «        ddd ¬¦  «        | _        t          j        | j        _        | j                             d¦  «         | j                             d¦  «         |                      ¦   «          d S )	NT)ÚparentsÚexist_okr   Fg      ð?)Úcheck_same_threadÚtimeoutÚisolation_levelzPRAGMA journal_mode=WALzPRAGMA foreign_keys=ON)ÚDEFAULT_DB_PATHr   ÚparentÚmkdirÚ	threadingÚLockÚ_lockÚ_write_countÚsqlite3ÚconnectÚstrÚ_connÚRowÚrow_factoryÚexecuteÚ_init_schema)Úselfr   s     ú./home/div/.hermes/hermes-agent/hermes_state.pyÚ__init__zSessionDB.__init__’   sÉ   € ØÐ1¥/ˆŒØŒÔ×!Ò!¨$¸Ð!Ñ>Ô>Ð>å”^Ñ%Ô%ˆŒ
ØˆÔÝ”_Ý�”ÑÔØ#ð ð !ð
ñ 
ô 
ˆŒ
õ ")¤ˆŒ
ÔØŒ
×ÒÐ4Ñ5Ô5Ð5ØŒ
×ÒÐ3Ñ4Ô4Ð4à×ÒÑÔÐÐÐó    ÚfnÚreturnc                 ó*  — d}t          | j        ¦  «        D �]f}	 | j        5  | j                             d¦  «         	  || j        ¦  «        }| j                             ¦   «          n:# t          $ r- 	 | j                             ¦   «          n# t          $ r Y nw xY w‚ w xY w	 ddd¦  «         n# 1 swxY w Y   | xj	        dz  c_	        | j	        | j
        z  dk    r|                      ¦   «          |c S # t          j        $ rx}t          |¦  «                             ¦   «         }d|v sd|v rI|}|| j        dz
  k     r9t!          j        | j        | j        ¦  «        }t)          j        |¦  «         Y d}~�Œ_‚ d}~ww xY w|pt          j        d¦  «        ‚)u‰  Execute a write transaction with BEGIN IMMEDIATE and jitter retry.

        *fn* receives the connection and should perform INSERT/UPDATE/DELETE
        statements.  The caller must NOT call ``commit()`` â€” that's handled
        here after *fn* returns.

        BEGIN IMMEDIATE acquires the WAL write lock at transaction start
        (not at commit time), so lock contention surfaces immediately.
        On ``database is locked``, we release the Python lock, sleep a
        random 20-150ms, and retry â€” breaking the convoy pattern that
        SQLite's built-in deterministic backoff creates.

        Returns whatever *fn* returns.
        NzBEGIN IMMEDIATEé   r   ÚlockedÚbusyz$database is locked after max retries)ÚrangeÚ_WRITE_MAX_RETRIESr   r"   r%   ÚcommitÚBaseExceptionÚrollbackÚ	Exceptionr   Ú_CHECKPOINT_EVERY_N_WRITESÚ_try_wal_checkpointr   ÚOperationalErrorr!   ÚlowerÚrandomÚuniformÚ_WRITE_RETRY_MIN_SÚ_WRITE_RETRY_MAX_SÚtimeÚsleep)r'   r+   Úlast_errÚattemptÚresultÚexcÚerr_msgÚjitters           r(   Ú_execute_writezSessionDB._execute_write¬   s/  € ð )-ˆÝ˜TÔ4Ñ5Ô5ð 	ñ 	ˆGðØ”Zð 
ð 
Ø”J×&Ò&Ð'8Ñ9Ô9Ð9ðØ!#  D¤J¡¤˜Øœ
×)Ò)Ñ+Ô+Ð+Ð+øÝ(ð ð ð ð!Ø œJ×/Ò/Ñ1Ô1Ð1Ð1øÝ(ð !ð !ð !Ø ˜Dð!øøøàðøøøð ,ð	
ð 
ð 
ñ 
ô 
ð 
ð 
ð 
ð 
ð 
ð 
øøøð 
ð 
ð 
ð 
ð Ð!Ô! QÑ&Ð!Ô!ØÔ$ tÔ'FÑFÈ!ÒKÐKØ×,Ò,Ñ.Ô.Ð.Ø���øÝÔ+ð ð ð Ý˜c™(œ(Ÿ.š.Ñ*Ô*�Ø˜wÐ&Ð&¨&°GÐ*;Ð*;Ø"�HØ Ô!8¸1Ñ!<Ò<Ð<Ý!'¤Ø Ô3Ø Ô3ñ"ô "˜õ œ
 6Ñ*Ô*Ð*Ø ˜˜˜™àøøøøðøøøð ð 
�'Ô2Ø2ñ
ô 
ð 	
s|   ›C5¢B/¾)A(Á'B/Á(
BÁ3BÂBÂ
B	ÂBÂB	ÂBÂB/Â#C5Â/B3	Â3C5Â6B3	Â7;C5Ã5E<ÄA,E7Å6E7Å7E<c                 ó.  — 	 | j         5  | j                             d¦  «                             ¦   «         }|r4|d         dk    r(t                               d|d         |d         ¦  «         ddd¦  «         dS # 1 swxY w Y   dS # t          $ r Y dS w xY w)a2  Best-effort PASSIVE WAL checkpoint.  Never blocks, never raises.

        Flushes committed WAL frames back into the main DB file for any
        frames that no other connection currently needs.  Keeps the WAL
        from growing unbounded when many processes hold persistent
        connections.
        úPRAGMA wal_checkpoint(PASSIVE)r.   r   z(WAL checkpoint: %d/%d pages checkpointedé   N)r   r"   r%   ÚfetchoneÚloggerÚdebugr6   )r'   rC   s     r(   r8   zSessionDB._try_wal_checkpointà   sù   € ð	Ø”ð ð Øœ×+Ò+Ø4ñô ç’(‘*”*ð ð ð ˜f Qœi¨!šm˜mÝ—L’LØBØ˜qœ	 6¨!¤9ñô ð ðð ð ñ ô ð ð ð ð ð ð ð øøøð ð ð ð ð ð øõ ð 	ð 	ð 	ØˆDˆDð	øøøs5   ‚B ‰A#A9Á,B Á9A=Á=B Â A=ÂB Â
BÂBc                 óì   — | j         5  | j        rL	 | j                             d¦  «         n# t          $ r Y nw xY w| j                             ¦   «          d| _        ddd¦  «         dS # 1 swxY w Y   dS )z©Close the database connection.

        Attempts a PASSIVE WAL checkpoint first so that exiting processes
        help keep the WAL file from growing unbounded.
        rI   N)r   r"   r%   r6   Úclose©r'   s    r(   rO   zSessionDB.closeõ   sÔ   € ð ŒZð 	"ð 	"ØŒzð "ðØ”J×&Ò&Ð'GÑHÔHÐHÐHøÝ ð ð ð Ø�Dðøøøà”
× Ò Ñ"Ô"Ð"Ø!�”
ð	"ð 	"ð 	"ñ 	"ô 	"ð 	"ð 	"ð 	"ð 	"ð 	"ð 	"ð 	"øøøð 	"ð 	"ð 	"ð 	"ð 	"ð 	"s,   ˆA)‘,«A)¬
9¶A)¸9¹#A)Á)A-Á0A-c                 ól  — | j                              ¦   «         }|                     t          ¦  «         |                     d¦  «         |                     ¦   «         }|€|                     dt          f¦  «         �n¬t          |t          j	        ¦  «        r|d         n|d         }|dk     rA	 |                     d¦  «         n# t          j
        $ r Y nw xY w|                     d¦  «         |d	k     rA	 |                     d
¦  «         n# t          j
        $ r Y nw xY w|                     d¦  «         |dk     rA	 |                     d¦  «         n# t          j
        $ r Y nw xY w|                     d¦  «         |dk     rhg d¢}|D ]L\  }}	 |                     dd¦  «        }|                     d|› d|› �¦  «         Œ8# t          j
        $ r Y ŒIw xY w|                     d¦  «         |dk     rddD ]L\  }}		 |                     dd¦  «        }
|                     d|
› d|	› �¦  «         Œ8# t          j
        $ r Y ŒIw xY w|                     d¦  «         |dk     rA	 |                     d¦  «         n# t          j
        $ r Y nw xY w|                     d¦  «         |dk     rA	 |                     d¦  «         n# t          j
        $ r Y nw xY w|                     d¦  «         |d k     rA	 |                     d!¦  «         n# t          j
        $ r Y nw xY w|                     d"¦  «         	 |                     d¦  «         n# t          j
        $ r Y nw xY w	 |                     d#¦  «         n/# t          j
        $ r |                     t          ¦  «         Y nw xY w| j                              ¦   «          dS )$z:Create tables and FTS if they don't exist, run migrations.z*SELECT version FROM schema_version LIMIT 1Nz/INSERT INTO schema_version (version) VALUES (?)Úversionr   rJ   z2ALTER TABLE messages ADD COLUMN finish_reason TEXTz%UPDATE schema_version SET version = 2é   z*ALTER TABLE sessions ADD COLUMN title TEXTz%UPDATE schema_version SET version = 3é   zfCREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_title_unique ON sessions(title) WHERE title IS NOT NULLz%UPDATE schema_version SET version = 4é   ))Úcache_read_tokensúINTEGER DEFAULT 0)Úcache_write_tokensrW   )Úreasoning_tokensrW   )Úbilling_providerÚTEXT)Úbilling_base_urlr[   )Úbilling_moder[   )Úestimated_cost_usdÚREAL)Úactual_cost_usdr_   )Úcost_statusr[   )Úcost_sourcer[   )Úpricing_versionr[   ú"z""z!ALTER TABLE sessions ADD COLUMN "z" z%UPDATE schema_version SET version = 5é   ))Ú	reasoningr[   )Úreasoning_detailsr[   )Úcodex_reasoning_itemsr[   z!ALTER TABLE messages ADD COLUMN "z%UPDATE schema_version SET version = 6é   z8ALTER TABLE messages ADD COLUMN "reasoning_content" TEXTz%UPDATE schema_version SET version = 7é   zBALTER TABLE sessions ADD COLUMN "api_call_count" INTEGER DEFAULT 0z%UPDATE schema_version SET version = 8r   z:ALTER TABLE messages ADD COLUMN "codex_message_items" TEXTz%UPDATE schema_version SET version = 9z"SELECT * FROM messages_fts LIMIT 0)r"   ÚcursorÚexecutescriptÚ
SCHEMA_SQLr%   rK   ÚSCHEMA_VERSIONÚ
isinstancer   r#   r9   ÚreplaceÚFTS_SQLr3   )r'   rk   ÚrowÚcurrent_versionÚnew_columnsÚnameÚcolumn_typeÚ	safe_nameÚcol_nameÚcol_typeÚsafes              r(   r&   zSessionDB._init_schema  sÔ  € à”×"Ò"Ñ$Ô$ˆà×Ò�ZÑ(Ô(Ð(ð 	�ŠÐCÑDÔDÐDØ�oŠoÑÔˆØˆ;Ø�NŠNÐLÍ~ÐN_Ñ`Ô`Ð`Ñ`å0:¸3ÅÄÑ0LÔ0LÐX˜c )œn˜nÐRUÐVWÔRXˆOØ Ò"Ð"ðØ—N’NÐ#WÑXÔXÐXÐXøÝÔ/ð ð ð Ø�Dðøøøà—’ÐFÑGÔGÐGØ Ò"Ð"ðØ—N’NÐ#OÑPÔPÐPÐPøÝÔ/ð ð ð Ø�Dðøøøà—’ÐFÑGÔGÐGØ Ò"Ð"ðØ—N’NðEñô ð ð øõ Ô/ð ð ð Ø�Dðøøøà—’ÐFÑGÔGÐGØ Ò"Ð"ðð ð �ð *5ð ð Ñ%�D˜+ðð %)§L¢L°°dÑ$;Ô$;˜	ØŸšÐ'eÈ9Ð'eÐ'eÐXcÐ'eÐ'eÑfÔfÐfÐføÝ"Ô3ð ð ð Ø˜ðøøøà—’ÐFÑGÔGÐGØ Ò"Ð"ð+ð ð Ñ&�H˜hð
Ø'×/Ò/°°TÑ:Ô:˜ØŸšØRÀÐRÐRÈÐRÐRñô ð ð øõ #Ô3ð ð ð Ø˜ðøøøà—’ÐFÑGÔGÐGØ Ò"Ð"ðØ—N’NÐ#]Ñ^Ô^Ð^Ð^øÝÔ/ð ð ð Ø�Dðøøøà—’ÐFÑGÔGÐGØ Ò"Ð"ðØ—N’NØ\ñô ð ð øõ Ô/ð ð ð Ø�Dðøøøà—’ÐFÑGÔGÐGØ Ò"Ð"ðØ—N’NÐ#_Ñ`Ô`Ð`Ð`øÝÔ/ð ð ð Ø�Dðøøøà—’ÐFÑGÔGÐGð	Ø�NŠNð=ñô ð ð øõ Ô'ð 	ð 	ð 	ØˆDð	øøøð	*Ø�NŠNÐ?Ñ@Ô@Ð@Ð@øÝÔ'ð 	*ð 	*ð 	*Ø× Ò ¥Ñ)Ô)Ð)Ð)Ð)ð	*øøøð 	Œ
×ÒÑÔÐÐÐs´   Â.C ÃCÃCÃ5D ÄDÄDÄ<E ÅE$Å#E$Æ1G Ç GÇGÇ81H*È*H<È;H<ÉI1 É1JÊJÊ"J8 Ê8K
Ë	K
Ë)K? Ë?LÌLÌ*M  Í MÍMÍM, Í,)NÎNÚ
session_idÚsourceÚmodelÚmodel_configÚsystem_promptÚuser_idÚparent_session_idc                 óT   ‡‡‡‡‡‡‡— ˆˆˆˆˆˆˆfd„}|                       |¦  «         ‰S )z4Create a new session record. Returns the session_id.c                 ó’   •— |                       d‰‰‰‰‰rt          j        ‰¦  «        nd ‰‰t          j        ¦   «         f¦  «         d S )Nz½INSERT OR IGNORE INTO sessions (id, source, user_id, model, model_config,
                   system_prompt, parent_session_id, started_at)
                   VALUES (?, ?, ?, ?, ?, ?, ?, ?))r%   ÚjsonÚdumpsr?   )Úconnr}   r~   r�   r{   r|   r   r€   s    €€€€€€€r(   Ú_doz%SessionDB.create_session.<locals>._do“  sa   ø€ Ø�LŠLð6ð ØØØØ0<ÐF•D”J˜|Ñ,Ô,Ð,À$Ø!Ø%Ý”I‘K”Kð	ñ	ô ð ð ð r*   ©rG   )	r'   r{   r|   r}   r~   r   r€   r�   r‡   s	    ``````` r(   Úcreate_sessionzSessionDB.create_sessionˆ  s\   øøøøøøø€ ð	ð 	ð 	ð 	ð 	ð 	ð 	ð 	ð 	ð 	ð 	ð  	×Ò˜CÑ Ô Ð ØÐr*   Ú
end_reasonc                 ó@   ‡‡— ˆˆfd„}|                       |¦  «         dS )aé  Mark a session as ended.

        No-ops when the session is already ended. The first end_reason wins:
        compression-split sessions must keep their ``end_reason = 'compression'``
        record even if a later stale ``end_session()`` call (e.g. from a
        desynced CLI session_id after ``/resume`` or ``/branch``) targets them
        with a different reason. Use ``reopen_session()`` first if you
        intentionally need to re-end a closed session with a new reason.
        c                 ó\   •— |                       dt          j        ¦   «         ‰‰f¦  «         d S )NzRUPDATE sessions SET ended_at = ?, end_reason = ? WHERE id = ? AND ended_at IS NULL©r%   r?   )r†   rŠ   r{   s    €€r(   r‡   z"SessionDB.end_session.<locals>._do°  s8   ø€ Ø�LŠLð4å”‘”˜j¨*Ð5ñô ð ð ð r*   Nrˆ   )r'   r{   rŠ   r‡   s    `` r(   Úend_sessionzSessionDB.end_session¦  s>   øø€ ð	ð 	ð 	ð 	ð 	ð 	ð 	×Ò˜CÑ Ô Ð Ð Ð r*   c                 ó<   ‡— ˆfd„}|                       |¦  «         dS )z6Clear ended_at/end_reason so a session can be resumed.c                 ó6   •— |                       d‰f¦  «         d S )NzCUPDATE sessions SET ended_at = NULL, end_reason = NULL WHERE id = ?©r%   ©r†   r{   s    €r(   r‡   z%SessionDB.reopen_session.<locals>._doº  s+   ø€ Ø�LŠLØUØ�ñô ð ð ð r*   Nrˆ   ©r'   r{   r‡   s    ` r(   Úreopen_sessionzSessionDB.reopen_session¸  s8   ø€ ð	ð 	ð 	ð 	ð 	ð
 	×Ò˜CÑ Ô Ð Ð Ð r*   c                 ó@   ‡‡— ˆˆfd„}|                       |¦  «         dS )z0Store the full assembled system prompt snapshot.c                 ó8   •— |                       d‰‰f¦  «         d S )Nz2UPDATE sessions SET system_prompt = ? WHERE id = ?r‘   )r†   r{   r   s    €€r(   r‡   z+SessionDB.update_system_prompt.<locals>._doÃ  s.   ø€ Ø�LŠLØDØ 
Ð+ñô ð ð ð r*   Nrˆ   )r'   r{   r   r‡   s    `` r(   Úupdate_system_promptzSessionDB.update_system_promptÁ  s>   øø€ ð	ð 	ð 	ð 	ð 	ð 	ð
 	×Ò˜CÑ Ô Ð Ð Ð r*   r   FÚinput_tokensÚoutput_tokensrV   rX   rY   r^   r`   ra   rb   rc   rZ   r\   r]   Úapi_call_countÚabsolutec                 ót   ‡‡— |rdŠndŠ|||||||	|	|
||||||||fŠˆˆfd„}|                       |¦  «         dS )u•  Update token counters and backfill model if not already set.

        When *absolute* is False (default), values are **incremented** â€” use
        this for per-API-call deltas (CLI path).

        When *absolute* is True, values are **set directly** â€” use this when
        the caller already holds cumulative totals (gateway path, where the
        cached agent accumulates across messages).
        a¤  UPDATE sessions SET
                   input_tokens = ?,
                   output_tokens = ?,
                   cache_read_tokens = ?,
                   cache_write_tokens = ?,
                   reasoning_tokens = ?,
                   estimated_cost_usd = COALESCE(?, 0),
                   actual_cost_usd = CASE
                       WHEN ? IS NULL THEN actual_cost_usd
                       ELSE ?
                   END,
                   cost_status = COALESCE(?, cost_status),
                   cost_source = COALESCE(?, cost_source),
                   pricing_version = COALESCE(?, pricing_version),
                   billing_provider = COALESCE(billing_provider, ?),
                   billing_base_url = COALESCE(billing_base_url, ?),
                   billing_mode = COALESCE(billing_mode, ?),
                   model = COALESCE(model, ?),
                   api_call_count = ?
                   WHERE id = ?a^  UPDATE sessions SET
                   input_tokens = input_tokens + ?,
                   output_tokens = output_tokens + ?,
                   cache_read_tokens = cache_read_tokens + ?,
                   cache_write_tokens = cache_write_tokens + ?,
                   reasoning_tokens = reasoning_tokens + ?,
                   estimated_cost_usd = COALESCE(estimated_cost_usd, 0) + COALESCE(?, 0),
                   actual_cost_usd = CASE
                       WHEN ? IS NULL THEN actual_cost_usd
                       ELSE COALESCE(actual_cost_usd, 0) + ?
                   END,
                   cost_status = COALESCE(?, cost_status),
                   cost_source = COALESCE(?, cost_source),
                   pricing_version = COALESCE(?, pricing_version),
                   billing_provider = COALESCE(billing_provider, ?),
                   billing_base_url = COALESCE(billing_base_url, ?),
                   billing_mode = COALESCE(billing_mode, ?),
                   model = COALESCE(model, ?),
                   api_call_count = COALESCE(api_call_count, 0) + ?
                   WHERE id = ?c                 ó4   •— |                       ‰‰¦  «         d S ©Nr‘   )r†   ÚparamsÚsqls    €€r(   r‡   z*SessionDB.update_token_counts.<locals>._do$  s   ø€ Ø�LŠL˜˜fÑ%Ô%Ð%Ð%Ð%r*   Nrˆ   )r'   r{   r˜   r™   r}   rV   rX   rY   r^   r`   ra   rb   rc   rZ   r\   r]   rš   r›   r‡   rŸ   r    s                      @@r(   Úupdate_token_countszSessionDB.update_token_countsÊ  s’   øø€ ð: ð )	#ð#ˆCˆCð*#ˆCð* ØØØØØØØØØØØØØØØØð#
ˆð&	&ð 	&ð 	&ð 	&ð 	&ð 	&à×Ò˜CÑ Ô Ð Ð Ð r*   Úunknownc                 óD   ‡‡‡— ˆˆˆfd„}|                       |¦  «         dS )a2  Ensure a session row exists, creating it with minimal metadata if absent.

        Used by _flush_messages_to_session_db to recover from a failed
        create_session() call (e.g. transient SQLite lock at agent startup).
        INSERT OR IGNORE is safe to call even when the row already exists.
        c                 ó^   •— |                       d‰‰‰t          j        ¦   «         f¦  «         d S )NzxINSERT OR IGNORE INTO sessions
                   (id, source, model, started_at)
                   VALUES (?, ?, ?, ?)r�   )r†   r}   r{   r|   s    €€€r(   r‡   z%SessionDB.ensure_session.<locals>._do4  s<   ø€ Ø�LŠLð*ð ˜V U­D¬I©K¬KÐ8ñ	ô ð ð ð r*   Nrˆ   )r'   r{   r|   r}   r‡   s    ``` r(   Úensure_sessionzSessionDB.ensure_session(  sD   øøø€ ð	ð 	ð 	ð 	ð 	ð 	ð 	ð 	×Ò˜CÑ Ô Ð Ð Ð r*   c                 óÆ   — | j         5  | j                             d|f¦  «        }|                     ¦   «         }ddd¦  «         n# 1 swxY w Y   |rt	          |¦  «        ndS )zGet a session by ID.z#SELECT * FROM sessions WHERE id = ?N©r   r"   r%   rK   Údict©r'   r{   rk   rr   s       r(   Úget_sessionzSessionDB.get_session=  s¥   € àŒZð 	$ð 	$Ø”Z×'Ò'Ø5¸
°}ñô ˆFð —/’/Ñ#Ô#ˆCð		$ð 	$ð 	$ñ 	$ô 	$ð 	$ð 	$ð 	$ð 	$ð 	$ð 	$øøøð 	$ð 	$ð 	$ð 	$ð
  Ð)�t�C‰yŒyˆy TÐ)ó   ˆ1AÁA	ÁA	Úsession_id_or_prefixc                 ó®  — |                       |¦  «        }|r|d         S |                     dd¦  «                             dd¦  «                             dd¦  «        }| j        5  | j                             d|› d�f¦  «        }d	„ |                     ¦   «         D ¦   «         }d
d
d
¦  «         n# 1 swxY w Y   t          |¦  «        dk    r|d         S d
S )a*  Resolve an exact or uniquely prefixed session ID to the full ID.

        Returns the exact ID when it exists. Otherwise treats the input as a
        prefix and returns the single matching session ID if the prefix is
        unambiguous. Returns None for no matches or ambiguous prefixes.
        Úidú\ú\\ú%ú\%Ú_ú\_zSSELECT id FROM sessions WHERE id LIKE ? ESCAPE '\' ORDER BY started_at DESC LIMIT 2c                 ó   — g | ]
}|d          ‘ŒS )r®   © ©Ú.0rr   s     r(   ú
<listcomp>z0SessionDB.resolve_session_id.<locals>.<listcomp>\  s   € Ð>Ð>Ð> S�s˜4”yÐ>Ð>Ð>r*   Nr.   r   )rª   rp   r   r"   r%   ÚfetchallÚlen)r'   r¬   ÚexactÚescapedrk   Úmatchess         r(   Úresolve_session_idzSessionDB.resolve_session_idF  s'  € ð × Ò Ð!5Ñ6Ô6ˆØð 	Ø˜”;Ðð !ßŠW�T˜6Ñ"Ô"ßŠW�S˜%Ñ Ô ßŠW�S˜%Ñ Ô ð	 	ð ŒZð 	?ð 	?Ø”Z×'Ò'ØfØ���Ð ñô ˆFð ?Ð>¨F¯OªOÑ,=Ô,=Ð>Ñ>Ô>ˆGð	?ð 	?ð 	?ñ 	?ô 	?ð 	?ð 	?ð 	?ð 	?ð 	?ð 	?øøøð 	?ð 	?ð 	?ð 	?õ ˆw‰<Œ<˜1ÒÐØ˜1”:ÐØˆts   Á%>B/Â/B3Â6B3éd   Útitlec                 óR  — | sdS t          j        dd| ¦  «        }t          j        dd|¦  «        }t          j        dd|¦  «                             ¦   «         }|sdS t          |¦  «        t          j        k    r-t          dt          |¦  «        › dt          j        › d	�¦  «        ‚|S )
a  Validate and sanitize a session title.

        - Strips leading/trailing whitespace
        - Removes ASCII control characters (0x00-0x1F, 0x7F) and problematic
          Unicode control chars (zero-width, RTL/LTR overrides, etc.)
        - Collapses internal whitespace runs to single spaces
        - Normalizes empty/whitespace-only strings to None
        - Enforces MAX_TITLE_LENGTH

        Returns the cleaned title string or None.
        Raises ValueError if the title exceeds MAX_TITLE_LENGTH after cleaning.
        Nz [\x00-\x08\x0b\x0c\x0e-\x1f\x7f]Ú zB[\u200b-\u200f\u2028-\u202e\u2060-\u2069\ufeff\ufffc\ufff9-\ufffb]z\s+Ú zTitle too long (z chars, max ú))ÚreÚsubÚstripr»   r   ÚMAX_TITLE_LENGTHÚ
ValueError)rÁ   Úcleaneds     r(   Úsanitize_titlezSessionDB.sanitize_titled  s¼   € ð ð 	Ø�4õ
 ”&Ð<¸bÀ%ÑHÔHˆõ ”&ØQØ�ñ
ô 
ˆõ ”&˜  gÑ.Ô.×4Ò4Ñ6Ô6ˆàð 	Ø�4åˆw‰<Œ<�)Ô4Ò4Ð4ÝØZ¥3 w¡<¤<ÐZÐZ½YÔ=WÐZÐZÐZñô ð ð ˆr*   c                 ór   ‡‡— |                       ‰¦  «        Šˆˆfd„}|                      |¦  «        }|dk    S )aL  Set or update a session's title.

        Returns True if session was found and title was set.
        Raises ValueError if title is already in use by another session,
        or if the title fails validation (too long, invalid characters).
        Empty/whitespace-only strings are normalized to None (clearing the title).
        c                 óØ   •— ‰rI|                       d‰‰f¦  «        }|                     ¦   «         }|rt          d‰› d|d         › �¦  «        ‚|                       d‰‰f¦  «        }|j        S )Nz3SELECT id FROM sessions WHERE title = ? AND id != ?zTitle 'z' is already in use by session r®   z*UPDATE sessions SET title = ? WHERE id = ?)r%   rK   rÊ   Úrowcount)r†   rk   Úconflictr{   rÁ   s      €€r(   r‡   z(SessionDB.set_session_title.<locals>._do™  s–   ø€ Øð 
àŸšØIØ˜JÐ'ñô �ð "Ÿ?š?Ñ,Ô,�Øð Ý$ØX %ÐXÐXÈÐQUÌÐXÐXñô ð ð —\’\Ø<Ø˜
Ð#ñô ˆFð ”?Ð"r*   r   )rÌ   rG   )r'   r{   rÁ   r‡   rÏ   s    ``  r(   Úset_session_titlezSessionDB.set_session_title�  sV   øø€ ð ×#Ò# EÑ*Ô*ˆð	#ð 	#ð 	#ð 	#ð 	#ð 	#ð" ×&Ò& sÑ+Ô+ˆØ˜!Š|Ðr*   c                 ó¸   — | j         5  | j                             d|f¦  «        }|                     ¦   «         }ddd¦  «         n# 1 swxY w Y   |r|d         ndS )z%Get the title for a session, or None.z'SELECT title FROM sessions WHERE id = ?NrÁ   ©r   r"   r%   rK   r©   s       r(   Úget_session_titlezSessionDB.get_session_title­  s£   € àŒZð 	$ð 	$Ø”Z×'Ò'Ø9¸J¸=ñô ˆFð —/’/Ñ#Ô#ˆCð		$ð 	$ð 	$ñ 	$ô 	$ð 	$ð 	$ð 	$ð 	$ð 	$ð 	$øøøð 	$ð 	$ð 	$ð 	$ð
  #Ð,ˆs�7Œ|ˆ|¨Ð,r«   c                 óÆ   — | j         5  | j                             d|f¦  «        }|                     ¦   «         }ddd¦  «         n# 1 swxY w Y   |rt	          |¦  «        ndS )z?Look up a session by exact title. Returns session dict or None.z&SELECT * FROM sessions WHERE title = ?Nr§   )r'   rÁ   rk   rr   s       r(   Úget_session_by_titlezSessionDB.get_session_by_title¶  s¥   € àŒZð 	$ð 	$Ø”Z×'Ò'Ø8¸5¸(ñô ˆFð —/’/Ñ#Ô#ˆCð		$ð 	$ð 	$ñ 	$ô 	$ð 	$ð 	$ð 	$ð 	$ð 	$ð 	$øøøð 	$ð 	$ð 	$ð 	$ð
  Ð)�t�C‰yŒyˆy TÐ)r«   c                 ó„  — |                       |¦  «        }|                     dd¦  «                             dd¦  «                             dd¦  «        }| j        5  | j                             d|› d�f¦  «        }|                     ¦   «         }d	d	d	¦  «         n# 1 swxY w Y   |r|d
         d         S |r|d         S d	S )ad  Resolve a title to a session ID, preferring the latest in a lineage.

        If the exact title exists, returns that session's ID.
        If not, searches for "title #N" variants and returns the latest one.
        If the exact title exists AND numbered variants exist, returns the
        latest numbered variant (the most recent continuation).
        r¯   r°   r±   r²   r³   r´   zaSELECT id, title, started_at FROM sessions WHERE title LIKE ? ESCAPE '\' ORDER BY started_at DESCú #%Nr   r®   )rÖ   rp   r   r"   r%   rº   )r'   rÁ   r¼   r½   rk   Únumbereds         r(   Úresolve_session_by_titlez"SessionDB.resolve_session_by_title¿  s  € ð ×)Ò)¨%Ñ0Ô0ˆð —-’-  fÑ-Ô-×5Ò5°c¸5ÑAÔA×IÒIÈ#ÈuÑUÔUˆØŒZð 	)ð 	)Ø”Z×'Ò'ðJà���Ð"ñô ˆFð
 —’Ñ(Ô(ˆHð	)ð 	)ð 	)ñ 	)ô 	)ð 	)ð 	)ð 	)ð 	)ð 	)ð 	)øøøð 	)ð 	)ð 	)ð 	)ð ð 	à˜A”;˜tÔ$Ð$Øð 	Ø˜”;ÐØˆts   Á4BÂBÂ"BÚ
base_titlec           	      óN  — t          j        d|¦  «        }|r|                     d¦  «        }n|}|                     dd¦  «                             dd¦  «                             dd¦  «        }| j        5  | j                             d	||› d
�f¦  «        }d„ |                     ¦   «         D ¦   «         }ddd¦  «         n# 1 swxY w Y   |s|S d}|D ]I}t          j        d|¦  «        }	|	r0t          |t          |	                     d¦  «        ¦  «        ¦  «        }ŒJ|› d|dz   › �S )u×   Generate the next title in a lineage (e.g., "my session" â†’ "my session #2").

        Strips any existing " #N" suffix to find the base name, then finds
        the highest existing number and increments.
        z^(.*?) #(\d+)$r.   r¯   r°   r±   r²   r³   r´   zESELECT title FROM sessions WHERE title = ? OR title LIKE ? ESCAPE '\'rØ   c                 ó   — g | ]
}|d          ‘ŒS )rÁ   r¶   r·   s     r(   r¹   z7SessionDB.get_next_title_in_lineage.<locals>.<listcomp>ñ  s   € ÐBÐBÐB¨˜˜GœÐBÐBÐBr*   Nz^.* #(\d+)$z #)
rÆ   ÚmatchÚgrouprp   r   r"   r%   rº   ÚmaxÚint)
r'   rÛ   rÞ   Úbaser½   rk   ÚexistingÚmax_numÚtÚms
             r(   Úget_next_title_in_lineagez#SessionDB.get_next_title_in_lineageÜ  s•  € õ ”Ð*¨JÑ7Ô7ˆØð 	Ø—;’;˜q‘>”>ˆDˆDàˆDð —,’,˜t VÑ,Ô,×4Ò4°S¸%Ñ@Ô@×HÒHÈÈeÑTÔTˆØŒZð 	Cð 	CØ”Z×'Ò'ØXØ˜'���Ð'ñô ˆFð CÐB°·²Ñ0AÔ0AÐBÑBÔBˆHð	Cð 	Cð 	Cñ 	Cô 	Cð 	Cð 	Cð 	Cð 	Cð 	Cð 	Cøøøð 	Cð 	Cð 	Cð 	Cð ð 	ØˆKð ˆØð 	8ð 	8ˆAÝ”˜¨Ñ+Ô+ˆAØð 8Ý˜g¥s¨1¯7ª7°1©:¬:¡¤Ñ7Ô7�øàÐ'Ð'˜' A™+Ð'Ð'Ð's   Á5?C Ã CÃCc                 óê   — |}t          d¦  «        D ]`}| j        5  | j                             d||f¦  «        }|                     ¦   «         }ddd¦  «         n# 1 swxY w Y   |€|c S |d         }Œa|S )a»  Walk the compression-continuation chain forward and return the tip.

        A compression continuation is a child session where:
        1. The parent's ``end_reason = 'compression'``
        2. The child was created AFTER the parent was ended (started_at >= ended_at)

        The second condition distinguishes compression continuations from
        delegate subagents or branch children, which can also have a
        ``parent_session_id`` but were created while the parent was still live.

        Returns the session_id of the latest continuation in the chain, or the
        input ``session_id`` if it isn't part of a compression chain (or if the
        input itself doesn't exist).
        rÀ   zÂSELECT id FROM sessions WHERE parent_session_id = ?   AND started_at >= (      SELECT ended_at FROM sessions       WHERE id = ? AND end_reason = 'compression'  ) ORDER BY started_at DESC LIMIT 1Nr®   )r1   r   r"   r%   rK   )r'   r{   Úcurrentr³   rk   rr   s         r(   Úget_compression_tipzSessionDB.get_compression_tipÿ  sÔ   € ð ˆõ �s‘”ð 	 ð 	 ˆAØ”ð (ð (Øœ×+Ò+ð7ð ˜gÐ&ñ	ô 	�ð —o’oÑ'Ô'�ð(ð (ð (ñ (ô (ð (ð (ð (ð (ð (ð (øøøð (ð (ð (ð (ð ˆ{Ø���Ø˜$”iˆGˆGØˆs   ›2AÁA	Á A	é   TÚexclude_sourcesÚlimitÚoffsetÚinclude_childrenÚproject_compression_tipsc                 ó  — g }g }|s|                      d¦  «         |r*|                      d¦  «         |                      |¦  «         |rMd                     d„ |D ¦   «         ¦  «        }	|                      d|	› d�¦  «         |                     |¦  «         |rdd                     |¦  «        › �nd	}
d
|
› d�}|                     ||g¦  «         | j        5  | j                             ||¦  «        }|                     ¦   «         }ddd¦  «         n# 1 swxY w Y   g }|D ]}}t          |¦  «        }|                     dd	¦  «         	                    ¦   «         }|r(|dd…         }|t          |¦  «        dk    rdnd	z   |d<   nd	|d<   |                      |¦  «         Œ~|rç|såg }|D ]Þ}|                     d¦  «        dk    r|                      |¦  «         Œ1|                      |d         ¦  «        }||d         k    r|                      |¦  «         Œn|                      |¦  «        }|s|                      |¦  «         Œ›t          |¦  «        }dD ]}||v r||         ||<   Œ|d         |d<   |                      |¦  «         Œß|}|S )u  List sessions with preview (first user message) and last active timestamp.

        Returns dicts with keys: id, source, model, title, started_at, ended_at,
        message_count, preview (first 60 chars of first user message),
        last_active (timestamp of last message).

        Uses a single query with correlated subqueries instead of N+2 queries.

        By default, child sessions (subagent runs, compression continuations)
        are excluded.  Pass ``include_children=True`` to include them.

        With ``project_compression_tips=True`` (default), sessions that are
        roots of compression chains are projected forward to their latest
        continuation â€” one logical conversation = one list entry, showing the
        live continuation's id/message_count/title/last_active. This prevents
        compressed continuations from being invisible to users while keeping
        delegate subagents and branches hidden. Pass ``False`` to return the
        raw root rows (useful for admin/debug UIs).
        zÁ(s.parent_session_id IS NULL OR EXISTS (SELECT 1 FROM sessions p            WHERE p.id = s.parent_session_id            AND p.end_reason = 'branched'            AND s.started_at >= p.ended_at))zs.source = ?Ú,c              3   ó   K  — | ]}d V — ŒdS ©ú?Nr¶   ©r¸   r³   s     r(   ú	<genexpr>z/SessionDB.list_sessions_rich.<locals>.<genexpr>T  s"   è è € Ð#AÐ#A¨A CÐ#AÐ#AÐ#AÐ#AÐ#AÐ#Ar*   ús.source NOT IN (rÅ   zWHERE ú AND rÃ   ah  
            SELECT s.*,
                COALESCE(
                    (SELECT SUBSTR(REPLACE(REPLACE(m.content, X'0A', ' '), X'0D', ' '), 1, 63)
                     FROM messages m
                     WHERE m.session_id = s.id AND m.role = 'user' AND m.content IS NOT NULL
                     ORDER BY m.timestamp, m.id LIMIT 1),
                    ''
                ) AS _preview_raw,
                COALESCE(
                    (SELECT MAX(m2.timestamp) FROM messages m2 WHERE m2.session_id = s.id),
                    s.started_at
                ) AS last_active
            FROM sessions s
            zM
            ORDER BY s.started_at DESC
            LIMIT ? OFFSET ?
        NÚ_preview_rawé<   ú...ÚpreviewrŠ   Úcompressionr®   )
r®   Úended_atrŠ   Úmessage_countÚtool_call_countrÁ   Úlast_activerý   r}   r   Ú_lineage_root_id)ÚappendÚjoinÚextendr   r"   r%   rº   r¨   ÚpoprÈ   r»   Úgetrê   Ú_get_session_rich_row)r'   r|   rì   rí   rî   rï   rð   Úwhere_clausesrŸ   ÚplaceholdersÚ	where_sqlÚqueryrk   ÚrowsÚsessionsrr   ÚsÚrawÚtextÚ	projectedÚtip_idÚtip_rowÚmergedÚkeys                           r(   Úlist_sessions_richzSessionDB.list_sessions_rich#  sY  € ð8 ˆØˆàð 	ð × Ò ð?ñô ð ð ð 	"Ø× Ò  Ñ0Ô0Ð0Ø�MŠM˜&Ñ!Ô!Ð!Øð 	+ØŸ8š8Ð#AÐ#A°Ð#AÑ#AÔ#AÑAÔAˆLØ× Ò Ð!D°\Ð!DÐ!DÐ!DÑEÔEÐEØ�MŠM˜/Ñ*Ô*Ð*à>KÐSÐ:˜WŸ\š\¨-Ñ8Ô8Ð:Ð:Ð:ÐQSˆ	ðð ðð ð ˆð$ 	�Š�u˜f�oÑ&Ô&Ð&ØŒZð 	%ð 	%Ø”Z×'Ò'¨¨vÑ6Ô6ˆFØ—?’?Ñ$Ô$ˆDð	%ð 	%ð 	%ñ 	%ô 	%ð 	%ð 	%ð 	%ð 	%ð 	%ð 	%øøøð 	%ð 	%ð 	%ð 	%ð ˆØð 		ð 		ˆCÝ�S‘	”	ˆAà—%’%˜¨Ñ+Ô+×1Ò1Ñ3Ô3ˆCØð "Ø˜3˜B˜3”x�Ø#µ°C±´¸2²° u uÀ2ÑF��)‘�à!��)‘Ø�OŠO˜AÑÔÐÐð $ð 	!Ð,<ð 	!ØˆIØð )ð )�Ø—5’5˜Ñ&Ô&¨-Ò7Ð7Ø×$Ò$ QÑ'Ô'Ð'ØØ×1Ò1°!°D´'Ñ:Ô:�Ø˜Q˜tœWÒ$Ð$Ø×$Ò$ QÑ'Ô'Ð'ØØ×4Ò4°VÑ<Ô<�Øð Ø×$Ò$ QÑ'Ô'Ð'Øõ ˜a™œ�ðð 3ð 3�Cð
 ˜g�~�~Ø&-¨c¤l˜˜s™øØ-.¨t¬W�Ð)Ñ*Ø× Ò  Ñ(Ô(Ð(Ð(Ø ˆHàˆs   Ã0DÄDÄDc                 ó|  — d}| j         5  | j                             ||f¦  «        }|                     ¦   «         }ddd¦  «         n# 1 swxY w Y   |sdS t	          |¦  «        }|                     dd¦  «                             ¦   «         }|r(|dd…         }|t          |¦  «        dk    rdndz   |d<   nd|d<   |S )z¬Fetch a single session with the same enriched columns as
        ``list_sessions_rich`` (preview + last_active). Returns None if the
        session doesn't exist.
        a  
            SELECT s.*,
                COALESCE(
                    (SELECT SUBSTR(REPLACE(REPLACE(m.content, X'0A', ' '), X'0D', ' '), 1, 63)
                     FROM messages m
                     WHERE m.session_id = s.id AND m.role = 'user' AND m.content IS NOT NULL
                     ORDER BY m.timestamp, m.id LIMIT 1),
                    ''
                ) AS _preview_raw,
                COALESCE(
                    (SELECT MAX(m2.timestamp) FROM messages m2 WHERE m2.session_id = s.id),
                    s.started_at
                ) AS last_active
            FROM sessions s
            WHERE s.id = ?
        Nrú   rÃ   rû   rü   rý   )r   r"   r%   rK   r¨   r  rÈ   r»   )r'   r{   r  rk   rr   r  r  r  s           r(   r	  zSessionDB._get_session_rich_rowŸ  s  € ð
ˆð  ŒZð 	$ð 	$Ø”Z×'Ò'¨°
¨}Ñ=Ô=ˆFØ—/’/Ñ#Ô#ˆCð	$ð 	$ð 	$ñ 	$ô 	$ð 	$ð 	$ð 	$ð 	$ð 	$ð 	$øøøð 	$ð 	$ð 	$ð 	$ð ð 	Ø�4Ý�‰IŒIˆØ�eŠe�N BÑ'Ô'×-Ò-Ñ/Ô/ˆØð 	Ø�s˜�s”8ˆDØ­C°©H¬H°rªM¨M 5 5¸rÑBˆAˆi‰LˆLàˆAˆi‰LØˆs   Š1AÁAÁAÚroleÚcontentÚ	tool_nameÚ
tool_callsÚtool_call_idÚtoken_countÚfinish_reasonrf   Úreasoning_contentrg   rh   Úcodex_message_itemsc                 ó€  ‡‡‡‡‡‡‡‡	‡
‡‡‡‡‡— |rt          j        |¦  «        ndŠ|rt          j        |¦  «        ndŠ|rt          j        |¦  «        ndŠ|rt          j        |¦  «        ndŠdŠ|�&t          |t          ¦  «        rt	          |¦  «        ndŠˆˆˆˆˆˆ	ˆ
ˆˆˆˆˆˆˆfd„}|                      |¦  «        S )zË
        Append a message to a session. Returns the message row ID.

        Also increments the session's message_count (and tool_call_count
        if role is 'tool' or tool_calls is present).
        Nr   r.   c                 óì   •— |                       d‰‰‰‰‰‰t          j        ¦   «         ‰‰‰‰	‰
‰‰f¦  «        }|j        }‰dk    r|                       d‰‰f¦  «         n|                       d‰f¦  «         |S )NaW  INSERT INTO messages (session_id, role, content, tool_call_id,
                   tool_calls, tool_name, timestamp, token_count, finish_reason,
                   reasoning, reasoning_content, reasoning_details, codex_reasoning_items,
                   codex_message_items)
                   VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)r   z€UPDATE sessions SET message_count = message_count + 1,
                       tool_call_count = tool_call_count + ? WHERE id = ?zBUPDATE sessions SET message_count = message_count + 1 WHERE id = ?)r%   r?   Ú	lastrowid)r†   rk   Úmsg_idÚcodex_items_jsonÚcodex_message_items_jsonr  r   Únum_tool_callsrf   r!  Úreasoning_details_jsonr  r{   r  r  Útool_calls_jsonr  s      €€€€€€€€€€€€€€r(   r‡   z%SessionDB.append_message.<locals>._doð  sº   ø€ Ø—\’\ðHð ØØØ Ø#ØÝ”I‘K”KØØ!ØØ%Ø*Ø$Ø,ðñô ˆFð. Ô%ˆFð  Ò!Ð!Ø—’ðMà# ZÐ0ñô ð ð ð —’ØXØ�Mñô ð ð ˆMr*   )r„   r…   ro   Úlistr»   rG   )r'   r{   r  r  r  r  r  r  r   rf   r!  rg   rh   r"  r‡   r'  r(  r)  r*  r+  s    ```` `````    @@@@@r(   Úappend_messagezSessionDB.append_messageÆ  s.  øøøøøøøøøøøøøø€ ð2 !ð+�DŒJÐ(Ñ)Ô)Ð)Ø&*ð 	ð %ð/�DŒJÐ,Ñ-Ô-Ð-Ø*.ð 	ð #ð-�DŒJÐ*Ñ+Ô+Ð+Ø(,ð 	!ð 5?ÐH�$œ* ZÑ0Ô0Ð0ÀDˆð ˆØÐ!Ý0:¸:ÅtÑ0LÔ0LÐS�S ™_œ_˜_ÐRSˆNð&	ð &	ð &	ð &	ð &	ð &	ð &	ð &	ð &	ð &	ð &	ð &	ð &	ð &	ð &	ð &	ð &	ð &	ðP ×"Ò" 3Ñ'Ô'Ð'r*   c                 óØ  — | j         5  | j                             d|f¦  «        }|                     ¦   «         }ddd¦  «         n# 1 swxY w Y   g }|D ]•}t	          |¦  «        }|                     d¦  «        rZ	 t          j        |d         ¦  «        |d<   n;# t          j        t          f$ r" t                               d¦  «         g |d<   Y nw xY w|                     |¦  «         Œ–|S )z6Load all messages for a session, ordered by timestamp.zBSELECT * FROM messages WHERE session_id = ? ORDER BY timestamp, idNr  zDFailed to deserialize tool_calls in get_messages, falling back to [])r   r"   r%   rº   r¨   r  r„   ÚloadsÚJSONDecodeErrorÚ	TypeErrorrL   Úwarningr  )r'   r{   rk   r  rC   rr   Úmsgs          r(   Úget_messageszSessionDB.get_messages  sG  € àŒZð 	%ð 	%Ø”Z×'Ò'ØTØ�ñô ˆFð —?’?Ñ$Ô$ˆDð	%ð 	%ð 	%ñ 	%ô 	%ð 	%ð 	%ð 	%ð 	%ð 	%ð 	%øøøð 	%ð 	%ð 	%ð 	%ð ˆØð 	ð 	ˆCÝ�s‘)”)ˆCØ�wŠw�|Ñ$Ô$ð +ð+Ý(,¬
°3°|Ô3DÑ(EÔ(E�C˜Ñ%Ð%øÝÔ,­iÐ8ð +ð +ð +Ý—N’NÐ#iÑjÔjÐjØ(*�C˜Ñ%Ð%Ð%ð+øøøð �MŠM˜#ÑÔÐÐØˆs#   ˆ1AÁA	ÁA	Á;BÂ5CÃCc                 ó^  — |s|S | j         5  	 | j                             d|f¦  «                             ¦   «         }n# t          $ r |cY cddd¦  «         S w xY w|�|cddd¦  «         S |}|h}t          d¦  «        D �]}	 | j                             d|f¦  «                             ¦   «         }n # t          $ r |cY c cddd¦  «         S w xY w|€|c cddd¦  «         S t          |d¦  «        r|d         n|d         }|r||v r|c cddd¦  «         S |                     |¦  «         	 | j                             d|f¦  «                             ¦   «         }n # t          $ r |cY c cddd¦  «         S w xY w|�|c cddd¦  «         S |}�Œ	 ddd¦  «         n# 1 swxY w Y   |S )uÓ  Redirect a resume target to the descendant session that holds the messages.

        Context compression ends the current session and forks a new child session
        (linked via ``parent_session_id``). The flush cursor is reset, so the
        child is where new messages actually land â€” the parent ends up with
        ``message_count = 0`` rows unless messages had already been flushed to
        it before compression. See #15000.

        This helper walks ``parent_session_id`` forward from ``session_id`` and
        returns the first descendant in the chain that has at least one message
        row. If the original session already has messages, or no descendant
        has any, the original ``session_id`` is returned unchanged.

        The chain is always walked via the child whose ``started_at`` is
        latest; that matches the single-chain shape that compression creates.
        A depth cap (32) guards against accidental loops in malformed data.
        z3SELECT 1 FROM messages WHERE session_id = ? LIMIT 1Né    z]SELECT id FROM sessions WHERE parent_session_id = ? ORDER BY started_at DESC, id DESC LIMIT 1Úkeysr®   r   )r   r"   r%   rK   r6   r1   ÚhasattrÚadd)	r'   r{   rr   ré   Úseenr³   Ú	child_rowÚchild_idÚmsg_rows	            r(   Úresolve_resume_session_idz#SessionDB.resolve_resume_session_id.  sZ  € ð$ ð 	ØÐàŒZð )	#ð )	#ð"Ø”j×(Ò(ØIØ�Mñô ÷ ’(‘*”*ð �øõ ð "ð "ð "Ø!Ð!Ð!ð)	#ð )	#ð )	#ð )	#ñ )	#ô )	#ð )	#ð )	#ð"øøøàˆØ!ð)	#ð )	#ð )	#ð )	#ñ )	#ô )	#ð )	#ð )	#ð !ˆGØ�9ˆDÝ˜2‘Y”Yð #ñ #�ð&Ø $¤
× 2Ò 2ðDð !˜
ñ	!ô !÷
 ’h‘j”jð �Iøõ !ð &ð &ð &Ø%Ð%Ð%Ð%Ð%ð3)	#ð )	#ð )	#ð )	#ñ )	#ô )	#ð )	#ð )	#ð0&øøøàÐ$Ø%Ð%Ð%ð7)	#ð )	#ð )	#ð )	#ñ )	#ô )	#ð )	#ð )	#õ8 /6°iÀÑ.HÔ.HÐZ˜9 Tœ?˜?ÈiÐXYÌl�Øð & 8¨tÐ#3Ð#3Ø%Ð%Ð%ð=)	#ð )	#ð )	#ð )	#ñ )	#ô )	#ð )	#ð )	#ð> —’˜Ñ"Ô"Ð"ð&Ø"œj×0Ò0ØMØ!˜ñô ÷ ’h‘j”jð �Gøõ !ð &ð &ð &Ø%Ð%Ð%Ð%Ð%ðM)	#ð )	#ð )	#ð )	#ñ )	#ô )	#ð )	#ð )	#ðJ&øøøàÐ&Ø#�O�OðQ)	#ð )	#ð )	#ð )	#ñ )	#ô )	#ð )	#ð )	#ðR #�‘ð3#ð!)	#ð )	#ð )	#ñ )	#ô )	#ð )	#ð )	#ð )	#ð )	#ð )	#ð )	#øøøð )	#ð )	#ð )	#ð )	#ðT Ðs‘   ŒF"Ž.=¼F"½AÁ	F"ÁAÁF"Á+F"Â.B2Â1F"Â2CÂ>F"ÃCÃF"Ã$)F"ÄF"Ä0.EÅF"ÅE<Å+F"Å;E<Å<F"ÆF"Æ"F&Æ)F&c                 óÄ  — | j         5  | j                             d|f¦  «        }|                     ¦   «         }ddd¦  «         n# 1 swxY w Y   g }|D �]
}|d         |d         dœ}|d         r|d         |d<   |d         r|d         |d<   |d         rZ	 t	          j        |d         ¦  «        |d<   n;# t          j        t          f$ r" t           	                    d	¦  «         g |d<   Y nw xY w|d         d
k    �rL|d         r|d         |d<   |d         �|d         |d<   |d         rZ	 t	          j        |d         ¦  «        |d<   n;# t          j        t          f$ r" t           	                    d¦  «         d|d<   Y nw xY w|d         rZ	 t	          j        |d         ¦  «        |d<   n;# t          j        t          f$ r" t           	                    d¦  «         d|d<   Y nw xY w|d         rZ	 t	          j        |d         ¦  «        |d<   n;# t          j        t          f$ r" t           	                    d¦  «         d|d<   Y nw xY w| 
                    |¦  «         �Œ|S )z–
        Load messages in the OpenAI conversation format (role + content dicts).
        Used by the gateway to restore conversation history.
        zÐSELECT role, content, tool_call_id, tool_calls, tool_name, reasoning, reasoning_content, reasoning_details, codex_reasoning_items, codex_message_items FROM messages WHERE session_id = ? ORDER BY timestamp, idNr  r  ©r  r  r  r  r  zKFailed to deserialize tool_calls in conversation replay, falling back to []Ú	assistantrf   r!  rg   z=Failed to deserialize reasoning_details, falling back to Nonerh   zAFailed to deserialize codex_reasoning_items, falling back to Noner"  z?Failed to deserialize codex_message_items, falling back to None)r   r"   r%   rº   r„   r/  r0  r1  rL   r2  r  )r'   r{   rk   r  Úmessagesrr   r3  s          r(   Úget_messages_as_conversationz&SessionDB.get_messages_as_conversationo  s?  € ð
 ŒZð 	%ð 	%Ø”Z×'Ò'ðLð �ñô ˆFð —?’?Ñ$Ô$ˆDð	%ð 	%ð 	%ñ 	%ô 	%ð 	%ð 	%ð 	%ð 	%ð 	%ð 	%øøøð 	%ð 	%ð 	%ð 	%ð ˆØð &	!ñ &	!ˆCØ˜vœ;°3°y´>ÐBÐBˆCØ�>Ô"ð :Ø&)¨.Ô&9��NÑ#Ø�;Ôð 4Ø#& {Ô#3��KÑ Ø�<Ô ð +ð+Ý(,¬
°3°|Ô3DÑ(EÔ(E�C˜Ñ%Ð%øÝÔ,­iÐ8ð +ð +ð +Ý—N’NÐ#pÑqÔqÐqØ(*�C˜Ñ%Ð%Ð%ð+øøøð �6Œ{˜kÒ)Ñ)Ø�{Ô#ð 8Ø'*¨;Ô'7�C˜Ñ$ØÐ*Ô+Ð7Ø/2Ð3FÔ/G�CÐ+Ñ,ØÐ*Ô+ð 8ð8Ý37´:¸cÐBUÔ>VÑ3WÔ3W˜Ð/Ñ0Ð0øÝ Ô0µ)Ð<ð 8ð 8ð 8ÝŸšÐ'fÑgÔgÐgØ37˜Ð/Ñ0Ð0Ð0ð8øøøð Ð.Ô/ð <ð<Ý7;´zÀ#ÐF]ÔB^Ñ7_Ô7_˜Ð3Ñ4Ð4øÝ Ô0µ)Ð<ð <ð <ð <ÝŸšÐ'jÑkÔkÐkØ7;˜Ð3Ñ4Ð4Ð4ð<øøøð Ð,Ô-ð :ð:Ý59´ZÀÐDYÔ@ZÑ5[Ô5[˜Ð1Ñ2Ð2øÝ Ô0µ)Ð<ð :ð :ð :ÝŸšÐ'hÑiÔiÐiØ59˜Ð1Ñ2Ð2Ð2ð:øøøð �OŠO˜CÑ Ô Ð Ñ ØˆsY   ˆ1AÁA	ÁA	ÂB5Â55C-Ã,C-Ä,E
Å
5FÆFÆF,Æ,5G$Ç#G$Ç0HÈ5IÉIr  c                 ó<  ‡— g Šdt           j        dt          fˆfd„}t          j        d|| ¦  «        }t          j        dd|¦  «        }t          j        dd|¦  «        }t          j        d	d
|¦  «        }t          j        dd|                     ¦   «         ¦  «        }t          j        dd|                     ¦   «         ¦  «        }t          j        dd|¦  «        }t          ‰¦  «        D ]\  }}|                     d|› d�|¦  «        }Œ |                     ¦   «         S )aÛ  Sanitize user input for safe use in FTS5 MATCH queries.

        FTS5 has its own query syntax where characters like ``"``, ``(``, ``)``,
        ``+``, ``*``, ``{``, ``}`` and bare boolean operators (``AND``, ``OR``,
        ``NOT``) have special meaning.  Passing raw user input directly to
        MATCH can cause ``sqlite3.OperationalError``.

        Strategy:
        - Preserve properly paired quoted phrases (``"exact phrase"``)
        - Strip unmatched FTS5-special characters that would cause errors
        - Wrap unquoted hyphenated and dotted terms in quotes so FTS5
          matches them as exact phrases instead of splitting on the
          hyphen/dot (e.g. ``chat-send``, ``P2.2``, ``my-app.config.ts``)
        ræ   r,   c                 ó€   •— ‰                      |                      d¦  «        ¦  «         dt          ‰¦  «        dz
  › d�S )Nr   ú Qr.   ú )r  rß   r»   )ræ   Ú_quoted_partss    €r(   Ú_preserve_quotedz8SessionDB._sanitize_fts5_query.<locals>._preserve_quoted¿  s?   ø€ Ø× Ò  §¢¨¡¤Ñ,Ô,Ð,Ø7�3˜}Ñ-Ô-°Ñ1Ð7Ð7Ð7Ð7r*   z"[^"]*"z
[+{}()\"^]rÄ   z\*+Ú*z(^|\s)\*z\1z(?i)^(AND|OR|NOT)\b\s*rÃ   z(?i)\s+(AND|OR|NOT)\s*$z\b(\w+(?:[.-]\w+)+)\bz"\1"rF  rG  )rÆ   ÚMatchr!   rÇ   rÈ   Ú	enumeraterp   )r  rI  Ú	sanitizedÚiÚquotedrH  s        @r(   Ú_sanitize_fts5_queryzSessionDB._sanitize_fts5_query«  s-  ø€ ð$ !ˆð	8¥¤ð 	8­Sð 	8ð 	8ð 	8ð 	8ð 	8ð 	8õ ”F˜:Ð'7¸Ñ?Ô?ˆ	õ ”F˜=¨#¨yÑ9Ô9ˆ	õ ”F˜6 3¨	Ñ2Ô2ˆ	Ý”F˜;¨¨yÑ9Ô9ˆ	õ ”FÐ4°b¸)¿/º/Ñ:KÔ:KÑLÔLˆ	Ý”FÐ5°r¸9¿?º?Ñ;LÔ;LÑMÔMˆ	õ ”FÐ3°W¸iÑHÔHˆ	õ # =Ñ1Ô1ð 	Cð 	C‰IˆAˆvØ!×)Ò)¨/°!¨/¨/¨/¸6ÑBÔBˆIˆIà�ŠÑ Ô Ð r*   r  c                 ó  — | D ]~}t          |¦  «        }d|cxk    rdk    s]n d|cxk    rdk    sNn d|cxk    rdk    s?n d|cxk    rdk    s0n d	|cxk    rd
k    s!n d|cxk    rdk    sn d|cxk    rdk    rn Œ{ dS ŒdS )zBCheck if text contains CJK (Chinese, Japanese, Korean) characters.i N  iÿŸ  i 4  i¿M  i   iß¦ i 0  i?0  i@0  iŸ0  i 0  iÿ0  i ¬  i¯×  TF)Úord)r  ÚchÚcps      r(   Ú_contains_cjkzSessionDB._contains_cjká  s  € ð ð 		ð 		ˆBÝ�R‘”ˆBØ˜"Ð&Ð&Ò&Ð& Ò&Ð&Ð&Ð&Ø˜"Ð&Ð&Ò&Ð& Ò&Ð&Ð&Ð&Ø˜2Ð(Ð(Ò(Ð( Ò(Ð(Ð(Ð(Ø˜"Ð&Ð&Ò&Ð& Ò&Ð&Ð&Ð&Ø˜"Ð&Ð&Ò&Ð& Ò&Ð&Ð&Ð&Ø˜"Ð&Ð&Ò&Ð& Ò&Ð&Ð&Ð&Ø˜"Ð&Ð&Ò&Ð& Ò&Ð&Ð&Ð&Ð&Ø�t�tð 'àˆur*   Úsource_filterÚrole_filterc                 óž  — |r|                      ¦   «         sg S |                      |¦  «        }|sg S dg}|g}|�Md                     d„ |D ¦   «         ¦  «        }	|                     d|	› d�¦  «         |                     |¦  «         |�Md                     d„ |D ¦   «         ¦  «        }
|                     d|
› d�¦  «         |                     |¦  «         |rMd                     d	„ |D ¦   «         ¦  «        }|                     d
|› d�¦  «         |                     |¦  «         d                     |¦  «        }|                     ||g¦  «         d|› d�}| j        5  	 | j                             ||¦  «        }d„ |                     ¦   «         D ¦   «         }n<# t          j
        $ r* |                      |¦  «        sg cY cddd¦  «         S g }Y nw xY wddd¦  «         n# 1 swxY w Y   |�s¼|                      |¦  «        �r¦|                      d¦  «                              ¦   «         }dg}d|› d�g}|�K|                     dd                     d„ |D ¦   «         ¦  «        › d�¦  «         |                     |¦  «         |�K|                     dd                     d„ |D ¦   «         ¦  «        › d�¦  «         |                     |¦  «         |rK|                     d
d                     d„ |D ¦   «         ¦  «        › d�¦  «         |                     |¦  «         dd                     |¦  «        › d�}|                     ||g¦  «         |g|z   }| j        5  | j                             ||¦  «        }d„ |                     ¦   «         D ¦   «         }ddd¦  «         n# 1 swxY w Y   |D ]ƒ}	 | j        5  | j                             d|d         |d         f¦  «        }d„ |                     ¦   «         D ¦   «         }ddd¦  «         n# 1 swxY w Y   ||d<   Œo# t          $ r g |d<   Y Œ€w xY w|D ]}|                     dd¦  «         Œ|S )a²  
        Full-text search across session messages using FTS5.

        Supports FTS5 query syntax:
          - Simple keywords: "docker deployment"
          - Phrases: '"exact phrase"'
          - Boolean: "docker OR kubernetes", "python NOT java"
          - Prefix: "deploy*"

        Returns matching messages with session metadata, content snippet,
        and surrounding context (1 message before and after the match).
        zmessages_fts MATCH ?Nrò   c              3   ó   K  — | ]}d V — ŒdS rô   r¶   rö   s     r(   r÷   z,SessionDB.search_messages.<locals>.<genexpr>  s"   è è € Ð*FÐ*F°1¨3Ð*FÐ*FÐ*FÐ*FÐ*FÐ*Fr*   zs.source IN (rÅ   c              3   ó   K  — | ]}d V — ŒdS rô   r¶   rö   s     r(   r÷   z,SessionDB.search_messages.<locals>.<genexpr>  s"   è è € Ð+IÐ+I°A¨CÐ+IÐ+IÐ+IÐ+IÐ+IÐ+Ir*   rø   c              3   ó   K  — | ]}d V — ŒdS rô   r¶   rö   s     r(   r÷   z,SessionDB.search_messages.<locals>.<genexpr>  s"   è è € Ð(BÐ(B°¨Ð(BÐ(BÐ(BÐ(BÐ(BÐ(Br*   zm.role IN (rù   a  
            SELECT
                m.id,
                m.session_id,
                m.role,
                snippet(messages_fts, 0, '>>>', '<<<', '...', 40) AS snippet,
                m.content,
                m.timestamp,
                m.tool_name,
                s.source,
                s.model,
                s.started_at AS session_started
            FROM messages_fts
            JOIN messages m ON m.id = messages_fts.rowid
            JOIN sessions s ON s.id = m.session_id
            WHERE z@
            ORDER BY rank
            LIMIT ? OFFSET ?
        c                 ó,   — g | ]}t          |¦  «        ‘ŒS r¶   ©r¨   r·   s     r(   r¹   z-SessionDB.search_messages.<locals>.<listcomp>@  s   € ÐBÐBÐB¨�4 ™9œ9ÐBÐBÐBr*   rd   zm.content LIKE ?r±   c              3   ó   K  — | ]}d V — ŒdS rô   r¶   rö   s     r(   r÷   z,SessionDB.search_messages.<locals>.<genexpr>I  s"   è è € Ð:VÐ:VÀ1¸3Ð:VÐ:VÐ:VÐ:VÐ:VÐ:Vr*   c              3   ó   K  — | ]}d V — ŒdS rô   r¶   rö   s     r(   r÷   z,SessionDB.search_messages.<locals>.<genexpr>L  s"   è è € Ð>\Ð>\Àq¸sÐ>\Ð>\Ð>\Ð>\Ð>\Ð>\r*   c              3   ó   K  — | ]}d V — ŒdS rô   r¶   rö   s     r(   r÷   z,SessionDB.search_messages.<locals>.<genexpr>O  s"   è è € Ð8RÐ8RÀ¸Ð8RÐ8RÐ8RÐ8RÐ8RÐ8Rr*   a¿  
                SELECT m.id, m.session_id, m.role,
                       substr(m.content,
                              max(1, instr(m.content, ?) - 40),
                              120) AS snippet,
                       m.content, m.timestamp, m.tool_name,
                       s.source, s.model, s.started_at AS session_started
                FROM messages m
                JOIN sessions s ON s.id = m.session_id
                WHERE zX
                ORDER BY m.timestamp DESC
                LIMIT ? OFFSET ?
            c                 ó,   — g | ]}t          |¦  «        ‘ŒS r¶   r]  r·   s     r(   r¹   z-SessionDB.search_messages.<locals>.<listcomp>c  s   € ÐGÐGÐG¨�4 ™9œ9ÐGÐGÐGr*   aó  WITH target AS (
                               SELECT session_id, timestamp, id
                               FROM messages
                               WHERE id = ?
                           )
                           SELECT role, content
                           FROM (
                               SELECT m.id, m.timestamp, m.role, m.content
                               FROM messages m
                               JOIN target t ON t.session_id = m.session_id
                               WHERE (m.timestamp < t.timestamp)
                                  OR (m.timestamp = t.timestamp AND m.id < t.id)
                               ORDER BY m.timestamp DESC, m.id DESC
                               LIMIT 1
                           )
                           UNION ALL
                           SELECT role, content
                           FROM messages
                           WHERE id = ?
                           UNION ALL
                           SELECT role, content
                           FROM (
                               SELECT m.id, m.timestamp, m.role, m.content
                               FROM messages m
                               JOIN target t ON t.session_id = m.session_id
                               WHERE (m.timestamp > t.timestamp)
                                  OR (m.timestamp = t.timestamp AND m.id > t.id)
                               ORDER BY m.timestamp ASC, m.id ASC
                               LIMIT 1
                           )r®   c                 óD   — g | ]}|d          |d         pddd…         dœ‘ŒS )r  r  rÃ   NéÈ   r@  r¶   )r¸   Úrs     r(   r¹   z-SessionDB.search_messages.<locals>.<listcomp>‹  sF   € ð $ð $ð $àð "# 6¤¸¸)¼Ð8JÈÈDÈSÈDÔ7QÐRÐRð$ð $ð $r*   Úcontextr  )rÈ   rP  r  r  r  r   r"   r%   rº   r   r9   rU  r6   r  )r'   r  rV  rì   rW  rí   rî   r
  rŸ   Úsource_placeholdersÚexclude_placeholdersÚrole_placeholdersr  r    rk   r¾   Ú	raw_queryÚ
like_whereÚlike_paramsÚlike_sqlÚlike_cursorrÞ   Ú
ctx_cursorÚcontext_msgss                           r(   Úsearch_messageszSessionDB.search_messagesð  sL  € ð* ð 	˜EŸKšK™MœMð 	ØˆIà×)Ò)¨%Ñ0Ô0ˆØð 	ØˆIð 0Ð0ˆØ�wˆàÐ$Ø"%§(¢(Ð*FÐ*F¸Ð*FÑ*FÔ*FÑ"FÔ"FÐØ× Ò Ð!GÐ1DÐ!GÐ!GÐ!GÑHÔHÐHØ�MŠM˜-Ñ(Ô(Ð(àÐ&Ø#&§8¢8Ð+IÐ+I¸Ð+IÑ+IÔ+IÑ#IÔ#IÐ Ø× Ò Ð!LÐ5IÐ!LÐ!LÐ!LÑMÔMÐMØ�MŠM˜/Ñ*Ô*Ð*àð 	'Ø #§¢Ð(BÐ(B°kÐ(BÑ(BÔ(BÑ BÔ BÐØ× Ò Ð!CÐ/@Ð!CÐ!CÐ!CÑDÔDÐDØ�MŠM˜+Ñ&Ô&Ð&à—L’L Ñ/Ô/ˆ	Ø�Š�u˜f�oÑ&Ô&Ð&ðð ðð ð ˆð( ŒZð 
	Cð 
	Cð	CØœ×+Ò+¨C°Ñ8Ô8�ð CÐB°·²Ñ0AÔ0AÐBÑBÔB��øõ Ô+ð ð ð ð ×)Ò)¨%Ñ0Ô0ð Ø�I�Ið
	Cð 
	Cð 
	Cð 
	Cñ 
	Cô 
	Cð 
	Cð 
	Cð ���ðøøøð
	Cð 
	Cð 
	Cñ 
	Cô 
	Cð 
	Cð 
	Cð 
	Cð 
	Cð 
	Cð 
	Cøøøð 
	Cð 
	Cð 
	Cð 
	Cð ñ 	H˜4×-Ò-¨eÑ4Ô4ñ 	HØŸš CÑ(Ô(×.Ò.Ñ0Ô0ˆIØ,Ð-ˆJØ!1 YÐ!1Ð!1Ð!1Ð 2ˆKØÐ(Ø×!Ò!Ð"Y°#·(²(Ð:VÐ:VÈÐ:VÑ:VÔ:VÑ2VÔ2VÐ"YÐ"YÐ"YÑZÔZÐZØ×"Ò" =Ñ1Ô1Ð1ØÐ*Ø×!Ò!Ð"_°c·h²hÐ>\Ð>\ÈOÐ>\Ñ>\Ô>\Ñ6\Ô6\Ð"_Ð"_Ð"_Ñ`Ô`Ð`Ø×"Ò" ?Ñ3Ô3Ð3Øð 0Ø×!Ò!Ð"U°·²Ð8RÐ8RÀkÐ8RÑ8RÔ8RÑ0RÔ0RÐ"UÐ"UÐ"UÑVÔVÐVØ×"Ò" ;Ñ/Ô/Ð/ðð —|’| JÑ/Ô/ðð ð ˆHð ×Ò  v˜Ñ/Ô/Ð/à$˜+¨Ñ3ˆKØ”ð Hð HØ"œj×0Ò0°¸;ÑGÔG�ØGÐG°×0DÒ0DÑ0FÔ0FÐGÑGÔG�ðHð Hð Hñ Hô Hð Hð Hð Hð Hð Hð Høøøð Hð Hð Hð Hð ð *	&ð *	&ˆEð)&Ø”Zð %ð %Ø!%¤×!3Ò!3ð ð< ˜tœ e¨D¤kÐ2ñ? "ô  "�JðB$ð $à!+×!4Ò!4Ñ!6Ô!6ð$ñ $ô $�LðE%ð %ð %ñ %ô %ð %ð %ð %ð %ð %ð %øøøð %ð %ð %ð %ðL $0��iÑ Ð øÝð &ð &ð &Ø#%��iÑ Ð Ð ð&øøøð ð 	'ð 	'ˆEØ�IŠI�i Ñ&Ô&Ð&Ð&àˆs‹   ÅG"Å FÅ;G"Æ&GÇ G"ÇGÇG"ÇGÇG"Ç"G&Ç)G&Í:N!Î!N%Î(N%Î1PÎ8APÐ PÐP	ÐPÐP	ÐPÐP/Ð.P/c                 óü   — | j         5  |r| j                             d|||f¦  «        }n| j                             d||f¦  «        }d„ |                     ¦   «         D ¦   «         cddd¦  «         S # 1 swxY w Y   dS )z-List sessions, optionally filtered by source.zQSELECT * FROM sessions WHERE source = ? ORDER BY started_at DESC LIMIT ? OFFSET ?z@SELECT * FROM sessions ORDER BY started_at DESC LIMIT ? OFFSET ?c                 ó,   — g | ]}t          |¦  «        ‘ŒS r¶   r]  r·   s     r(   r¹   z-SessionDB.search_sessions.<locals>.<listcomp>«  s   € Ð;Ð;Ð; #•D˜‘I”IÐ;Ð;Ð;r*   N)r   r"   r%   rº   )r'   r|   rí   rî   rk   s        r(   Úsearch_sessionszSessionDB.search_sessions™  sÞ   € ð ŒZð 	<ð 	<Øð 	Øœ×+Ò+ØgØ˜U FÐ+ñô ��ð
 œ×+Ò+ØVØ˜F�Oñô �ð <Ð;¨¯ªÑ):Ô):Ð;Ñ;Ô;ð	<ð 	<ð 	<ð 	<ñ 	<ô 	<ð 	<ð 	<ð 	<ð 	<ð 	<ð 	<øøøð 	<ð 	<ð 	<ð 	<ð 	<ð 	<s   ˆAA1Á1A5Á8A5c                 óê   — | j         5  |r| j                             d|f¦  «        }n| j                             d¦  «        }|                     ¦   «         d         cddd¦  «         S # 1 swxY w Y   dS )z.Count sessions, optionally filtered by source.z.SELECT COUNT(*) FROM sessions WHERE source = ?zSELECT COUNT(*) FROM sessionsr   NrÓ   )r'   r|   rk   s      r(   Úsession_countzSessionDB.session_count±  sÁ   € àŒZð 	(ð 	(Øð MØœ×+Ò+ØDÀvÀiñô ��ð œ×+Ò+Ð,KÑLÔL�Ø—?’?Ñ$Ô$ QÔ'ð	(ð 	(ð 	(ð 	(ñ 	(ô 	(ð 	(ð 	(ð 	(ð 	(ð 	(ð 	(øøøð 	(ð 	(ð 	(ð 	(ð 	(ð 	(ó   ˆAA(Á(A,Á/A,c                 óê   — | j         5  |r| j                             d|f¦  «        }n| j                             d¦  «        }|                     ¦   «         d         cddd¦  «         S # 1 swxY w Y   dS )z2Count messages, optionally for a specific session.z2SELECT COUNT(*) FROM messages WHERE session_id = ?zSELECT COUNT(*) FROM messagesr   NrÓ   )r'   r{   rk   s      r(   r   zSessionDB.message_count¼  sÁ   € àŒZð 	(ð 	(Øð MØœ×+Ò+ØHÈ:È-ñô ��ð œ×+Ò+Ð,KÑLÔL�Ø—?’?Ñ$Ô$ QÔ'ð	(ð 	(ð 	(ð 	(ñ 	(ô 	(ð 	(ð 	(ð 	(ð 	(ð 	(ð 	(øøøð 	(ð 	(ð 	(ð 	(ð 	(ð 	(rv  c                 ón   — |                       |¦  «        }|sdS |                      |¦  «        }i |¥d|i¥S )z8Export a single session with all its messages as a dict.NrB  )rª   r4  )r'   r{   ÚsessionrB  s       r(   Úexport_sessionzSessionDB.export_sessionË  sJ   € à×"Ò" :Ñ.Ô.ˆØð 	Ø�4Ø×$Ò$ ZÑ0Ô0ˆØ0�'Ð0˜: xÐ0Ð0Ð0r*   c                 ó®   — |                       |d¬¦  «        }g }|D ]8}|                      |d         ¦  «        }|                     i |¥d|i¥¦  «         Œ9|S )z‹
        Export all sessions (with messages) as a list of dicts.
        Suitable for writing to a JSONL file for backup/analysis.
        i † )r|   rí   r®   rB  )rs  r4  r  )r'   r|   r  Úresultsry  rB  s         r(   Ú
export_allzSessionDB.export_allÓ  sq   € ð
 ×'Ò'¨v¸VÐ'ÑDÔDˆØˆØð 	>ð 	>ˆGØ×(Ò(¨°¬Ñ7Ô7ˆHØ�NŠNÐ<˜gÐ< z°8Ð<Ð<Ñ=Ô=Ð=Ð=Øˆr*   c                 ó<   ‡— ˆfd„}|                       |¦  «         dS )z9Delete all messages for a session and reset its counters.c                 ód   •— |                       d‰f¦  «         |                       d‰f¦  «         d S )Nú)DELETE FROM messages WHERE session_id = ?zGUPDATE sessions SET message_count = 0, tool_call_count = 0 WHERE id = ?r‘   r’   s    €r(   r‡   z%SessionDB.clear_messages.<locals>._doá  sJ   ø€ Ø�LŠLØ;¸j¸]ñô ð ð �LŠLØYØ�ñô ð ð ð r*   Nrˆ   r“   s    ` r(   Úclear_messageszSessionDB.clear_messagesß  s8   ø€ ð	ð 	ð 	ð 	ð 	ð 	×Ò˜CÑ Ô Ð Ð Ð r*   c                 ó8   ‡— ˆfd„}|                       |¦  «        S )zýDelete a session and all its messages.

        Child sessions are orphaned (parent_session_id set to NULL) rather
        than cascade-deleted, so they remain accessible independently.
        Returns True if the session was found and deleted.
        c                 ó   •— |                       d‰f¦  «        }|                     ¦   «         d         dk    rdS |                       d‰f¦  «         |                       d‰f¦  «         |                       d‰f¦  «         dS )Nz*SELECT COUNT(*) FROM sessions WHERE id = ?r   FzHUPDATE sessions SET parent_session_id = NULL WHERE parent_session_id = ?r€  ú!DELETE FROM sessions WHERE id = ?T)r%   rK   )r†   rk   r{   s     €r(   r‡   z%SessionDB.delete_session.<locals>._doò  s“   ø€ Ø—\’\Ø<¸z¸mñô ˆFð �ŠÑ Ô  Ô# qÒ(Ð(Ø�uà�LŠLð.à�ñô ð ð
 �LŠLÐDÀzÀmÑTÔTÐTØ�LŠLÐ<¸z¸mÑLÔLÐLØ�4r*   rˆ   r“   s    ` r(   Údelete_sessionzSessionDB.delete_sessionë  s2   ø€ ð	ð 	ð 	ð 	ð 	ð ×"Ò" 3Ñ'Ô'Ð'r*   éZ   Úolder_than_daysc                 ón   ‡‡— t          j         ¦   «         |dz  z
  Šˆˆfd„}|                      |¦  «        S )a  Delete sessions older than N days. Returns count of deleted sessions.

        Only prunes ended sessions (not active ones).  Child sessions outside
        the prune window are orphaned (parent_session_id set to NULL) rather
        than cascade-deleted.
        i€Q c                 óâ  •— ‰r|                       d‰‰f¦  «        }n|                       d‰f¦  «        }t          d„ |                     ¦   «         D ¦   «         ¦  «        }|sdS d                     dt	          |¦  «        z  ¦  «        }|                       d|› d�t          |¦  «        ¦  «         |D ]0}|                       d	|f¦  «         |                       d
|f¦  «         Œ1t	          |¦  «        S )NzkSELECT id FROM sessions
                       WHERE started_at < ? AND ended_at IS NOT NULL AND source = ?zESELECT id FROM sessions WHERE started_at < ? AND ended_at IS NOT NULLc              3   ó&   K  — | ]}|d          V — ŒdS )r®   Nr¶   r·   s     r(   r÷   z8SessionDB.prune_sessions.<locals>._do.<locals>.<genexpr>  s&   è è € ÐEÐE¨C˜c $œiÐEÐEÐEÐEÐEÐEr*   r   rò   rõ   zIUPDATE sessions SET parent_session_id = NULL WHERE parent_session_id IN (rÅ   r€  r„  )r%   Úsetrº   r  r»   r,  )r†   rk   Úsession_idsr  ÚsidÚcutoffr|   s        €€r(   r‡   z%SessionDB.prune_sessions.<locals>._do  s*  ø€ Øð 
ØŸšðWà˜VÐ$ñô ��ð ŸšØ[Ø�Iñô �õ ÐEÐE°6·?²?Ñ3DÔ3DÐEÑEÔEÑEÔEˆKàð Ø�qð Ÿ8š8 C­#¨kÑ*:Ô*:Ñ$:Ñ;Ô;ˆLØ�LŠLð?Ø/;ð?ð ?ð ?å�[Ñ!Ô!ñô ð ð #ð Jð J�Ø—’ÐHÈ3È&ÑQÔQÐQØ—’Ð@À3À&ÑIÔIÐIÐIÝ�{Ñ#Ô#Ð#r*   )r?   rG   )r'   r‡  r|   r‡   rŽ  s     ` @r(   Úprune_sessionszSessionDB.prune_sessions  sO   øø€ õ ”‘” °%Ñ 7Ñ8ˆð	$ð 	$ð 	$ð 	$ð 	$ð 	$ð< ×"Ò" 3Ñ'Ô'Ð'r*   r  c                 óø   — | j         5  | j                             d|f¦  «                             ¦   «         }ddd¦  «         n# 1 swxY w Y   |€dS t	          |t
          j        ¦  «        r|d         n|d         S )z1Read a value from the state_meta key/value store.z*SELECT value FROM state_meta WHERE key = ?NÚvaluer   )r   r"   r%   rK   ro   r   r#   )r'   r  rr   s      r(   Úget_metazSessionDB.get_meta.  s¸   € àŒZð 	ð 	Ø”*×$Ò$Ø<¸s¸fñô çŠh‰jŒjð ð	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ð 	ð 	ð 	øøøð 	ð 	ð 	ð 	ð ˆ;Ø�4Ý)¨#­w¬{Ñ;Ô;ÐGˆs�7Œ|ˆ|ÀÀQÄÐGs   ˆ/AÁAÁ
Ar‘  c                 ó@   ‡‡— ˆˆfd„}|                       |¦  «         dS )z0Write a value to the state_meta key/value store.c                 ó8   •— |                       d‰‰f¦  «         d S )NzgINSERT INTO state_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.valuer‘   )r†   r  r‘  s    €€r(   r‡   zSessionDB.set_meta.<locals>._do:  s0   ø€ Ø�LŠLðHà�e�ñô ð ð ð r*   Nrˆ   )r'   r  r‘  r‡   s    `` r(   Úset_metazSessionDB.set_meta8  s>   øø€ ð	ð 	ð 	ð 	ð 	ð 	ð 	×Ò˜CÑ Ô Ð Ð Ð r*   c                 óÒ   — | j         5  	 | j                             d¦  «         n# t          $ r Y nw xY w| j                             d¦  «         ddd¦  «         dS # 1 swxY w Y   dS )ui  Run VACUUM to reclaim disk space after large deletes.

        SQLite does not shrink the database file when rows are deleted â€”
        freed pages just get reused on the next insert. After a prune that
        removed hundreds of sessions, the file stays bloated unless we
        explicitly VACUUM.

        VACUUM rewrites the entire DB, so it's expensive (seconds per
        100MB) and cannot run inside a transaction. It also acquires an
        exclusive lock, so callers must ensure no other writers are
        active. Safe to call at startup before the gateway/CLI starts
        serving traffic.
        zPRAGMA wal_checkpoint(TRUNCATE)ÚVACUUMN)r   r"   r%   r6   rP   s    r(   ÚvacuumzSessionDB.vacuumD  sÅ   € ð ŒZð 	)ð 	)ðØ”
×"Ò"Ð#DÑEÔEÐEÐEøÝð ð ð Ø�ðøøøàŒJ×Ò˜xÑ(Ô(Ð(ð	)ð 	)ð 	)ñ 	)ô 	)ð 	)ð 	)ð 	)ð 	)ð 	)ð 	)ð 	)øøøð 	)ð 	)ð 	)ð 	)ð 	)ð 	)s,   ˆAŠ%¤A¥
2¯A±2²AÁA Á#A é   Úretention_daysÚmin_interval_hoursr˜  c                 óè  — ddddœ}	 |                       d¦  «        }t          j        ¦   «         }|r;	 t          |¦  «        }||z
  |dz  k     rd|d<   |S n# t          t          f$ r Y nw xY w|                      |¬¦  «        }||d	<   |rS|dk    rM	 |                      ¦   «          d|d
<   n2# t          $ r%}	t           	                    d|	¦  «         Y d}	~	nd}	~	ww xY w|  
                    dt          |¦  «        ¦  «         |dk    r't                               d|||d
         rdnd¦  «         nD# t          $ r7}	t           	                    d|	¦  «         t          |	¦  «        |d<   Y d}	~	nd}	~	ww xY w|S )uÆ  Idempotent auto-maintenance: prune old sessions + optional VACUUM.

        Records the last run timestamp in state_meta so subsequent calls
        within ``min_interval_hours`` no-op. Designed to be called once at
        startup from long-lived entrypoints (CLI, gateway, cron scheduler).

        Never raises. On any failure, logs a warning and returns a dict
        with ``"error"`` set.

        Returns a dict with keys:
          - ``"skipped"`` (bool) â€” true if within min_interval_hours of last run
          - ``"pruned"`` (int)   â€” number of sessions deleted
          - ``"vacuumed"`` (bool) â€” true if VACUUM ran
          - ``"error"`` (str, optional) â€” present only on failure
        Fr   )ÚskippedÚprunedÚvacuumedÚlast_auto_prunei  Tr�  )r‡  rž  rŸ  zstate.db VACUUM failed: %sNzDstate.db auto-maintenance: pruned %d session(s) older than %d days%sz	 + VACUUMrÃ   z$state.db auto-maintenance failed: %sÚerror)r’  r?   Úfloatr1  rÊ   r�  r˜  r6   rL   r2  r•  r!   Úinfo)
r'   rš  r›  r˜  rC   Úlast_rawÚnowÚlast_tsrž  rD   s
             r(   Úmaybe_auto_prune_and_vacuumz%SessionDB.maybe_auto_prune_and_vacuum[  sç  € ð* .3¸aÈUÐ!SÐ!Sˆð'	'à—}’}Ð%6Ñ7Ô7ˆHÝ”)‘+”+ˆCØð ðÝ# H™oœo�GØ˜W‘}Ð'9¸DÑ'@Ò@Ð@Ø,0˜˜yÑ)Ø%˜ð Aøõ "¥:Ð.ð ð ð Ø�Dðøøøð ×(Ò(¸Ð(ÑHÔHˆFØ%ˆF�8Ñð ð F˜& 1š*˜*ðFØ—K’K‘M”M�MØ)-�F˜:Ñ&Ð&øÝ ð Fð Fð FÝ—N’NÐ#?ÀÑEÔEÐEÐEÐEÐEÐEÐEøøøøðFøøøð
 �MŠMÐ+­S°©X¬XÑ6Ô6Ð6à˜ŠzˆzÝ—’ØZØØ"Ø#)¨*Ô#5Ð=�K�K¸2ñ	ô ð øøõ ð 	'ð 	'ð 	'å�NŠNÐAÀ3ÑGÔGÐGÝ! #™hœhˆF�7‰OˆOˆOˆOˆOˆOøøøøð	'øøøð
 ˆse   ˆ*D. ³!A ÁD. ÁA*Á'D. Á)A*Á*&D. ÂB+ Â*D. Â+
CÂ5CÃD. ÃCÃAD. Ä.
E/Ä8-E*Å*E/rž   )r,   N)NNNNN)r   r   Nr   r   r   NNNNNNNNr   F)r¢   N)NNrë   r   FT)NNNNNNNNNNN)NNNrë   r   )Nrë   r   )r†  N)r†  r™  T)@Ú__name__Ú
__module__Ú__qualname__Ú__doc__r2   r=   r>   r7   r   r)   r   r   Ú
Connectionr   rG   r8   rO   r&   r!   r   r   r‰   rŽ   r”   r—   rá   r	   r¢  Úboolr¡   r¥   rª   r¿   rÉ   ÚstaticmethodrÌ   rÑ   rÔ   rÖ   rÚ   rç   rê   r   r  r	  r-  r4  r>  rC  rP  rU  rp  rs  ru  r   rz  r}  r�  r…  r�  r’  r•  r˜  r§  r¶   r*   r(   r   r   {   só	  € € € € € ðð ð  ÐØÐØÐà!#Ððð  ð ð ð ð ð42
 ¨7Ô+=Ð*>ÀÐ*AÔ!Bð 2
Àqð 2
ð 2
ð 2
ð 2
ðhð ð ð ð*"ð "ð "ð~ð ~ð ~ðP Ø'+Ø!ØØ!%ðð àðð ðð ð	ð
 ˜3 ˜8”nðð ðð ðð ðð 
ðð ð ð ð<! cð !°sð !¸tð !ð !ð !ð !ð$!¨ð !°ð !ð !ð !ð !ð!¨sð !À3ð !È4ð !ð !ð !ð !ð ØØØ!"Ø"#Ø !Ø.2Ø+/Ø%)Ø%)Ø)-Ø*.Ø*.Ø&*ØØð%\!ð \!àð\!ð ð\!ð ð	\!ð
 ð\!ð ð\!ð  ð\!ð ð\!ð % UœOð\!ð " %œð\!ð ˜c”]ð\!ð ˜c”]ð\!ð " #œð\!ð # 3œ-ð\!ð # 3œ-ð\!ð  ˜s”mð!\!ð" ð#\!ð$ ð%\!ð& 
ð'\!ð \!ð \!ð \!ðB  Øð	!ð !àð!ð ð!ð ð	!ð
 
ð!ð !ð !ð !ð** cð *¨h°t¸CÀ¸H´~Ô.Fð *ð *ð *ð *ð°sð ¸xÈ¼}ð ð ð ð ð8 Ðàð)˜h sœmð )°¸´ð )ð )ð )ñ „\ð)ðV¨Cð ¸ð Àð ð ð ð ð:-¨Cð -°H¸S´Mð -ð -ð -ð -ð*¨#ð *°(¸4ÀÀSÀ¼>Ô2Jð *ð *ð *ð *ð¨cð °h¸s´mð ð ð ð ð:!(°Cð !(¸Cð !(ð !(ð !(ð !(ðF"¨cð "°h¸s´mð "ð "ð "ð "ðL Ø%)ØØØ!&Ø)-ðzð zàðzð ˜cœðzð ð	zð
 ðzð ðzð #'ðzð 
ˆd�3˜�8ŒnÔ	ðzð zð zð zðx!°ð !¸ÀÀcÈ3ÀhÄÔ8Pð !ð !ð !ð !ðV ØØØ ØØ!ØØ!%Ø!%Ø%)Ø#'ðR(ð R(àðR(ð ðR(ð ð	R(ð
 ðR(ð ðR(ð ðR(ð ðR(ð ðR(ð ðR(ð ðR(ð ðR(ð  #ðR(ð !ðR(ð 
ðR(ð R(ð R(ð R(ðh sð ¨t°D¸¸c¸´NÔ/Cð ð ð ð ð(?°Cð ?¸Cð ?ð ?ð ?ð ?ðB6°sð 6¸tÀDÈÈcÈÄNÔ?Sð 6ð 6ð 6ð 6ðx ð2! Cð 2!¨Cð 2!ð 2!ð 2!ñ „\ð2!ðj ð˜Cð  Dð ð ð ñ „\ðð" $(Ø%)Ø!%ØØðgð gàðgð ˜C”yðgð ˜cœð	gð
 ˜#”Yðgð ðgð ðgð 
ˆd�3˜�8ŒnÔ	ðgð gð gð gðV ØØð	<ð <àð<ð ð<ð ð	<ð
 
ˆd�3˜�8ŒnÔ	ð<ð <ð <ð <ð0	(ð 	( Cð 	(°3ð 	(ð 	(ð 	(ð 	(ð	(ð 	(¨ð 	(°sð 	(ð 	(ð 	(ð 	(ð1¨ð 1°¸$¸sÀC¸x¼.Ô1Ið 1ð 1ð 1ð 1ð
ð 
 ð 
°°T¸#¸s¸(´^Ô0Dð 
ð 
ð 
ð 
ð
!¨ð 
!°ð 
!ð 
!ð 
!ð 
!ð(¨ð (°ð (ð (ð (ð (ð0'(ð '(¨cð '(Àð '(Èsð '(ð '(ð '(ð '(ðVH˜Cð H H¨S¤Mð Hð Hð Hð Hð!˜Cð !¨ð !°ð !ð !ð !ð !ð)ð )ð )ð )ð2 !Ø"$Øð	?ð ?àð?ð  ð?ð ð	?ð
 
ˆc�3ˆhŒð?ð ?ð ?ð ?ð ?ð ?r*   r   )r«  r„   Úloggingr;   rÆ   r   r   r?   Úpathlibr   Úhermes_constantsr   Útypingr   r   r   r   r	   r
   Ú	getLoggerr¨  rL   r   r   rn   rm   rq   r   r¶   r*   r(   ú<module>r´     s(  ððð ð €€€Ø €€€Ø €€€Ø 	€	€	€	Ø €€€Ø Ð Ð Ð Ø €€€Ø Ð Ð Ð Ð Ð Ø ,Ð ,Ð ,Ð ,Ð ,Ð ,Ø ?Ð ?Ð ?Ð ?Ð ?Ð ?Ð ?Ð ?Ð ?Ð ?Ð ?Ð ?Ð ?Ð ?Ð ?Ð ?à	ˆÔ	˜8Ñ	$Ô	$€à€GˆC�L„L€à!�/Ñ#Ô# jÑ0€à€ð?€
ðB€ð,_ð _ð _ð _ð _ñ _ô _ð _ð _ð _r*   