
    +j^~                     V   d 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m	Z	m
Z
mZ ddlmZ  ej        e          Z eh d          Z ej        d          Z ej        d	          Z ej        d
          Zdede
e	eef                  fdZde
e	eef                  de
e	eef                  fdZdefdZd@dee         de	eef         fdZ	 d@dedee         dee	eef                  fdZ	 d@dedee         dee         fdZdadad Z 	 	 dAdedee         dee	eef                  dee         fdZ!dBdededefdZ"de#de$fd Z% ej        d!          Z& ej        d"ej'                  Z(dede
e	eef                  fd#Z)dedefd$Z*de#dee#         fd%Z+d&edefd'Z,d(ede
e	eef                  fd)Z-d*e
e#         d+e
e	eef                  ddfd,Z.d*e
e#         de
e/         fd-Z0d.edee1         fd/Z2d0e
e1         d1e
e1         de
ee/                  fd2Z3d*e
e#         d3e
e/         d+e
e	eef                  ddfd4Z4	 d@d5d6d7dedee         d8e/d9e$de
e#         f
d:Z5d5d6d7d;ed8e/d9e$de
e#         fd<Z6d5d6d7d=e
e#         d8e/d9e$de
e#         fd>Z7d@dee         de
e#         fd?Z8dS )Cu  
LightClaw history — read session transcripts and return history messages.
Mirrors: src/history/ (session-store.ts, message-parser.ts, session-reader.ts,
text-processing.ts).

Storage note: since Hermes "spec 002" the message transcript lives in SQLite
(``~/.hermes/state.db`` → table ``messages``), not in per-session ``.jsonl``
files.  ``sessions.json`` survives in both eras as a ``session_key →
session_id`` index.

To stay compatible with BOTH the new and the legacy builds we read in two
tiers (see ``read_session_history``):
  1. SQLite via :class:`hermes_state.SessionDB` (new builds).
  2. Per-session ``<session_id>.jsonl`` transcript files (legacy builds, or
     when SessionDB is unavailable / returns nothing).
    N)Path)AnyDictListOptional   )LOCALFILE_SCHEME>   customsessionmodel_changethinking_level_changeu<   用户发送了文件:\s*(?P<name>.+?)\s*\((?P<size>[^)]+)\)zT\[media attached:\s*(?P<path>\S+)\s*\((?P<mime>[^)]+)\)\s*\|\s*(?P<uri>[^\]]+?)\s*\]z---[0-9a-f-]+\.textreturnc                    | sg S g }t                               |           D ]d}|                    |                    d                                          |                    d                                          d           ed}t
                              |           D ]}|                    d                                          }|                    d                                          }|t          |          k     r|||         d<   |||         d<   nXd|v r|                    dd	          d
         n|}t          	                    d|          pd}|                    |||d           |d	z  }|S )u  Recover file attachment info from a previously-rendered message body.

    Returns a list of ``{name, mimeType?, size?, uri?}`` dicts, ordered as
    in TS ``extractFileAttachments``: first pass extracts entries from
    ``"用户发送了文件: ..."`` descriptions, then a second pass enriches
    them (in order) with the ``[media attached: ...]`` markers' MIME/URI
    fields, finally appending unmatched markers as standalone entries.
    namesize)r   r   r   mimeurimimeType/r   .file)r   r   r   )
_USER_FILE_DESC_REfinditerappendgroupstrip_MEDIA_ATTACH_RElenrsplit_TRAILING_HASH_REsub)r   filesm	media_idxr   r   bases          ;/home/agentuser/.hermes/plugins/lightclawbot/src/history.pyextract_file_attachmentsr)   7   s     	"$E((.. 8 8aggfoo3355ggfoo33557 7 	8 	8 	8 	8 I&&t,, 
 
wwv$$&&wwu~~##%%s5zz!!+/E)Z(&)E)U##-0CZZ3::c1%%b))SD$((d33=vDLL$DEEFFFQ		L    r$   c                 2   i }| D ]r}|                     d          pd}||v rC||         }dD ]7}|                     |          s |                     |          r||         ||<   8`t          |          ||<   st          |                                          S )zCMerge duplicate file entries by name (mirrors TS deduplicateFiles).r    )r   r   r   )getdictlistvalues)r$   seenfr   existingks         r(   deduplicate_filesr5   Y   s    &(D ! !uuV}}"4<<DzH0 ' '||A '15588 '"#A$HQK' aDJJr*   c                      t           j                            d          pt           j                            d          } t           j                            | d          S )z-Return the default Hermes sessions directory.HERMES_HOMEz	~/.hermessessions)osenvironr-   path
expanduserjoin)hermes_homes    r(   _default_sessions_dirr?   l   s@    *..//R273E3Ek3R3RK7<<Z000r*   sessions_dirc                    | pt                      }t          j                            |d          }	 t	          |dd          5 }t          j        |          }ddd           n# 1 swxY w Y   t          |t                    r|S nE# t          t
          j
        t          f$ r&}t                              d||           Y d}~nd}~ww xY wi S )u,   Load sessions.json → {session_key: entry}.sessions.jsonrutf-8encodingNz(Could not load sessions.json from %s: %s)r?   r9   r;   r=   openjsonload
isinstancer.   FileNotFoundErrorJSONDecodeErrorOSErrorloggerdebug)r@   dr;   r2   dataexcs         r(   load_session_storerS   r   s   /-//A7<<?++DL$g... 	 !9Q<<D	  	  	  	  	  	  	  	  	  	  	  	  	  	  	 dD!! 	K	t3W= L L L?sKKKKKKKKLIs;   B A%B %A))B ,A)-B C
$CC
session_keyc                 
   t          |          }|                    |           }|\|                                                                 }|                                D ]!\  }}|                                |k    r|} n"|S )zBFind the sessions.json entry for *session_key* (case-insensitive).)rS   r-   r   loweritems)rT   r@   storeentryrV   r4   vs          r(   _lookup_session_entryr[      s    
 |,,EIIk""E}!!##))++KKMM 	 	DAqwwyyE!! " Lr*   c                     t          | |          }|st                              d|            dS |                    d          p|                    d          pd}|st                              d|            dS |S )uG   Resolve *session_key* → ``session_id`` using the sessions.json index.z!No sessions.json entry for key=%sN
session_id	sessionIdr,   z,Entry for key=%s has no session_id/sessionId)r[   rN   rO   r-   )rT   r@   rY   r]   s       r(   resolve_session_idr_      s    
 "+|<<E 8+FFFt <((HEIIk,B,BHbJ C[QQQtr*   Fc                      t           t           S t          rdS 	 ddlm}   |             a t           S # t          $ r(}t
                              d|           daY d}~dS d}~ww xY w)zReturn a cached ``hermes_state.SessionDB`` instance, or ``None``.

    Imported lazily so the plugin still loads if the running Hermes build
    predates the SQLite session store.
    Nr   )	SessionDBz1[lightclaw] SessionDB unavailable for history: %sT)_session_db_session_db_failedhermes_statera   	ExceptionrN   warning)ra   rR   s     r(   _get_session_dbrg      s      t******ikk   JCPPP!ttttts   0 
A"AA"r]   rY   c                    | sdS |pt                      }|  d}|r|                    d          p|                    d          }|rbt          j                            |          r|nt          j                            ||          }t          j                            |          r|S t          j                            ||          }t          j                            |          r|S t          j                            |d          }t          j                            |          }	|	rG|	|k    rAt          j                            |	|          }
t          j                            |
          r|
S dS )a  Locate the ``<session_id>.jsonl`` transcript file (legacy builds).

    Fallback chain (mirrors session-store.ts):
      1. entry.sessionFile (absolute, or relative to sessions_dir)
      2. <sessions_dir>/<session_id>.jsonl
      3. sessions.json sibling directory (offline / downloaded sessions)
    Returns ``None`` when no transcript file exists (e.g. on new SQLite-only
    builds, which is the expected case there).
    N.jsonlsession_filesessionFilerB   )r?   r-   r9   r;   isabsr=   isfiledirname)r]   r@   rY   rP   
jsonl_namesfpp2sessions_json_pathsibling_dirp3s              r(   resolve_transcript_pathrv      sP     t/-//A&&&J  YY~&&B%))M*B*B 	gmmB''@RW\\!R-@-@Aw~~a    
a	$	$B	w~~b 	 a99'//"455K {a''W\\+z227>>" 	I4r*   r,   contentrolec                 v   t          | t                    r| S t          | t                    sdS g }| D ]r}t          |t                    s|                    dd          }|dv r@|                    dd          }t          |t                    r|                    |           sd                    |          S )z0Extract plain text from a message content field.r,   type)r   output_text
input_textr   
)rJ   strr/   r.   r-   r   r=   )rw   rx   partsrY   ttxts         r(   _extract_textr      s    '3 gt$$ rE " "%&& 	IIfb!!555))FB''C#s## "S!!!99Ur*   msgc                    |                      d          pd                                dk    rdS t          |                      dd                    }t          t	          j        d|                                                    S )z?Return True if a user-role message is actually system-injected.rx   r,   userFrw   z,^System:\s*\[\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})r-   rV   r   boolrematchr   )r   r   s     r(   _is_system_injectedr     sk    2$$&&&00uB//00DH$**,,WWXXXr*   z4\[(?P<name>[^\]]+)\]\((?P<uri>localfile://[^)\s]+)\)z1^[`\"']?MEDIA:\s*(?P<path>(?:~/|/)\S+)[`\"']?\s*$c                 d    | r	t           | vrg S d t                              |           D             S )zAExtract ``[name](localfile://...)`` links from assistant replies.c                 d    g | ]-}|                     d           |                     d          d.S )r   r   )r   r   )r   ).0r%   s     r(   
<listcomp>z,_extract_localfile_links.<locals>.<listcomp>+  sD        88  r*   )r	   _LOCALFILE_MD_REr   )r   s    r(   _extract_localfile_linksr   '  sI     #4//	 !**400   r*   c                 r    d| vr| S dt           j        dt          fd}t                              ||           S )u  Replace ``MEDIA:/path`` lines with ``localfile://`` Markdown links.

    Transcript JSONL stores the raw AI reply including MEDIA: tags (the
    framework strips them at delivery time via ``extract_media()``).  When
    serving history back to the front-end, we convert them to the same
    ``📎 [name](localfile:///path)`` format used by live delivery so the
    user sees clickable download links.
    zMEDIA:r%   r   c                     |                      d                                                              d          }t          j                            |          p|}d| dt           | dS )Nr;   z
"'`,.;:)}]u   📎 [z]())r   r   rstripr9   r;   basenamer	   )r%   r;   r   s      r(   _replacez2_convert_media_tags_to_localfile.<locals>._replace=  sd    wwv$$&&--m<<w%%-999 09$9999r*   )r   Matchr~   _MEDIA_TAG_REr#   )r   r   s     r(    _convert_media_tags_to_localfiler   1  sP     t:BH : : : : :
 Xt,,,r*   c                    |                      dd          }|sdS ddddddd}|                     |                                d          }t          |                      d	          |          }|                      d
          }t          |t          t
          f          r|nd}|s|dvrdS ||d}|||d
<   |dk    r$t          |          }|rt          |          |d<   n+|dk    r%t          |          }	t          |	          }
|
r|
|d<   |S )zuConvert a raw JSONL message object to a HistoryMessage dict.

    Returns None if the message should be skipped.
    rx   r,   Nr   	assistantsystemtool)r   r   r   r   
toolresulttool_resultrw   	timestamp)r   )rx   rw   r$   )
r-   rV   r   rJ   intfloatr)   r5   r   r   )r   role_rawrole_maprx   r   raw_timestampr   resultr$   converted_textlocalfile_linkss              r(   normalize_messager   E  sY   
 wwvr""H t   H <<((+66D++T22DGGK((M!+MC<!H!HRdI  D	))t  F '{
 v~~(.. 	7/66F7O			 :$??2>BB 	.-F7OMr*   transcript_pathc                 l    |                      d          r| dt          d                    dz   S | dz   S )u   Return the sidecar usage path for a given transcript jsonl path.

    Convention: ``<dir>/<session_id>.jsonl`` → ``<dir>/<session_id>.usage.jsonl``.
    ri   N.usage.jsonl)endswithr    )r   s    r(   _resolve_usage_log_pathr   }  sD    
 )) B/#h--/0>AA^++r*   r;   c                    t           j                            |           sg S g }	 t          | dd          5 }|D ]}|                                }|s	 t          j        |          }n# t
          j        $ r Y @w xY wt          |t                    sZ|
                    d          }|t          |t                    s|
                    d          }t          |t          t          f          s|                    |
                    d          t          |          |d           	 ddd           n# 1 swxY w Y   n5# t          $ r(}t                              d	| |           g cY d}~S d}~ww xY w|S )
u  Parse the usage sidecar jsonl into a list of entries.

    Each entry has shape::

        {"roundMsgId": str, "timestamp": int, "usage": dict | None}

    A ``usage`` of ``None`` denotes a *placeholder* line written by
    ``outbound._persist_turn_usage`` for turns where the tracker had no
    delta to report.  Placeholders are kept in the list to preserve
    ordinal alignment with transcript turn-end assistants — the attach
    step skips them rather than writing ``null`` onto a message.

    Malformed lines are skipped silently.  A missing file returns ``[]``
    — equivalent to "no usage data yet" (e.g. fresh sessions before the
    feature shipped).
    rC   rD   rE   usageNr   
roundMsgId)r   r   r   z#Failed to read usage sidecar %s: %s)r9   r;   rm   rG   r   rH   loadsrL   rJ   r.   r-   r   r   r   rM   rN   rf   )	r;   entriesr2   linestrippedobjr   tsrR   s	            r(   _read_usage_logr     s   " 7>>$ 	$&G$g... 	!  ::<< *X..CC+   H!#t,,  (($Zt-D-D$WW[))!"sEl33 "%'',"7"7"%b''"'       %	 	 	 	 	 	 	 	 	 	 	 	 	 	 	0    <dCHHH						 Nse   E D:A)(D:)A;8D::A;;B2D:.E :D>>E D>E 
E8E3-E83E8messagesusage_entriesc                     |sdS t                     }|sdS  fd|D             }d |D             }t          |          t          |          k    rt          d |D                       rrt          d |D                       rYt          ||          }t	          |          D ]7\  }}|||                             d          }|&| ||                  d<   8dS t           ||           dS )u  In-place: attach each usage entry to its matching turn-end assistant.

    "Turn-end assistant" = the last ``assistant`` of each turn (a ``user``
    message opens a turn; tool/system rows are transparent), mirroring
    openclaw's "one usage per turn, on the turn's last assistant".

    Matching strategy — **absolute-time anchoring** (preferred):
        Each sidecar entry carries the ``stop_typing`` timestamp of its round;
        each turn-end assistant carries its store timestamp.  We anchor every
        entry to the turn-end assistant *nearest in time* via an order-
        preserving (monotonic) minimum-cost assignment.  Because inter-turn
        gaps (tens of seconds) dwarf the intra-turn writer skew (sub-second),
        each entry lands on its own turn — and, crucially, the pairing stays
        correct when the two sides differ in count (a command round wrote no
        entry, or a real-but-zero-token round was skipped).  That count drift
        is exactly what pure ordinal alignment cannot survive: one missing or
        extra entry shifts every subsequent pairing.

    Fallback — **ordinal tail-alignment**:
        Used when timestamps are unusable on either side, or there are more
        entries than turn-ends (legacy data).  Pairs the i-th entry with the
        i-th turn-end counting from the end, so freshly-added turns at the tail
        line up and pre-feature turns at the head go without usage.

    Placeholder entries (``usage is None``, written when a real turn could not
    be measured) take part in matching so they *claim* their own slot and keep
    neighbours from drifting onto it, but no payload is attached — a null usage
    would mislead the renderer into showing "0 tokens" instead of "unknown".
    Nc                 `    g | ]*}t          |                             d                     +S r   _usage_ts_msr-   )r   ir   s     r(   r   z-_attach_usage_to_messages.<locals>.<listcomp>  s1    VVVQl8A;??;7788VVVr*   c                 R    g | ]$}t          |                    d                     %S r   r   )r   es     r(   r   z-_attach_usage_to_messages.<locals>.<listcomp>  s,    HHHQQUU;//00HHHr*   c              3      K   | ]}|d uV  	d S N r   r   s     r(   	<genexpr>z,_attach_usage_to_messages.<locals>.<genexpr>  s&      ..!......r*   c              3      K   | ]}|d uV  	d S r   r   r   s     r(   r   z,_attach_usage_to_messages.<locals>.<genexpr>  s&      00!000000r*   r   )_collect_turn_end_assistantsr    all_align_entries_by_time	enumerater-   _attach_usage_tail_ordinal)	r   r   turn_end_assistantsend_tsentry_ts
assignment	entry_posend_posr   s	   `        r(   _attach_usage_to_messagesr     sF   B  6x@@ VVVVBUVVVFHH-HHHH 	Mc"56666..v..... 	700x00000 	7 ,Hf==
"+J"7"7 	D 	DIw!),0099E}>CH(127;; x)<mLLLLLr*   c                     g }d}t          |           D ]B\  }}|                    d          }|dk    r||                    |           d}:|dk    r|}C||                    |           |S )a%  Return indices of each turn's last assistant message, in order.

    A ``user`` message opens a new turn; the last ``assistant`` seen before the
    next ``user`` (or end-of-list) is that turn's end.  ``tool`` / ``system``
    rows are transparent and leave the running pointer untouched.
    Nrx   r   r   )r   r-   r   )r   r   last_assistant_in_turnr   r%   rx   s         r(   r   r     s     &(,0(## ' '1uuV}}6>>%1#**+ABBB)-&;%&")""#9:::r*   r   c                     t          | t                    st          | t          t          f          sdS | dk     rt          |           dz  nt          |           S )a  Normalise a timestamp to epoch milliseconds, or ``None`` if unusable.

    Transcript rows store epoch *seconds* (e.g. ``1780387827.02``) while the
    usage sidecar stores epoch *milliseconds* (``int(time.time() * 1000)``).
    A value below ``1e12`` is therefore treated as seconds and scaled up, so
    both sides become directly comparable without the callsite assuming a unit.
    ``bool`` is rejected explicitly (it is an ``int`` subclass in Python).
    Ng   mBg     @@)rJ   r   r   r   )r   s    r(   r   r     sT     "d :b3,#?#? t!#d599vb		9r*   r   r   c           	         	
 t          |           t          |          c}
|dk    rg S t          d          		
fdt          |dz             D             }t          
dz             D ]}d|d         |<   t          d|dz             D ]u}t          |
dz             D ]_}||         |dz
           }||dz
           |dz
           t          | |dz
           ||dz
           z
            z   }||k     r|n|||         |<   `vdg|z  }|
}}|dk    r}|dk    rw||         |dz
           }||dz
           |dz
           t          | |dz
           ||dz
           z
            z   }||k    r|dz
  ||dz
  <   |dz  }|dz  }n|dz  }|dk    r|dk    w|S )u  Order-preserving minimum-cost assignment of entries onto turn-ends.

    Returns a list parallel to *entry_ts*: ``result[i]`` is the index into
    *end_ts* that entry ``i`` is anchored to (strictly increasing across i), or
    ``None`` if unassigned.  Cost is absolute time distance; the chosen
    turn-ends form an increasing subsequence — matching the fact that sidecar
    entries and turn-end assistants share one chronological order while the
    head of the transcript may pre-date the sidecar feature.

    Caller guarantees ``len(entry_ts) <= len(end_ts)`` so a full assignment of
    every entry exists.  Complexity ``O(p * q)`` with both small.
    r   infc                 "    g | ]}gd z   z  S )r   r   )r   _r   qs     r(   r   z*_align_entries_by_time.<locals>.<listcomp>:  s#    	0	0	0a3%1q5/	0	0	0r*   r   g        N)r    r   rangeabs)r   r   rq   dpjr   skiptaker   r   r   s            @@r(   r   r   $  s     x==#f++DAqAvv	
,,C 
1	0	0	0	05Q<<	0	0	0B1q5\\  1a1a!e__ 5 5q!a% 	5 	5Aa5Q<Da!e9QU#c(1q5/F1q5M*I&J&JJD#d{{ttBqE!HH	5
 (,fqjJaqA
a%%AEE!uQU|!a%yQ#hq1uoq1u&E"F"FF4<< !AJq1uFAFAAFA a%%AEE r*   r   c                    t          t          |          t          |                    }|dk    rdS || d         }|| d         }t          ||          D ],\  }}|                    d          |d         | |         d<   -dS )u   Fallback pairing: i-th entry ↔ i-th turn-end, counted from the end.

    Placeholder entries (``usage is None``) still occupy a slot to preserve
    ordinal alignment, but carry no payload.
    r   Nr   )minr    zipr-   )r   r   r   npaired_assistantspaired_entriesasst_idxrY   s           r(   r   r   Q  s     	C#$$c-&8&899AAvv+QBCC0"A233'N0.AA 5 5%99W%&+Gn7##5 5r*      Tlimit	chat_onlyr   r   c                   t          | |          }|s,t                              d| |pt                                 g S |                    d          p|                    d          pd}|st                              d|            g S t                      }|	 |                    |d          }n5# t          $ r(}t                              d	||           d}Y d}~nd}~ww xY w|rt          |||
          }		 |pt                      }
t          j
                            |
| d          }t          |          }|rt          |	|           n3# t          $ r&}t                              d||           Y d}~nd}~ww xY w|	S t                              d|           t          |||          }|st                              d| |           g S t                              d|           	 t!          |dd          5 }|                                }ddd           n# 1 swxY w Y   n5# t$          $ r(}t                              d||           g cY d}~S d}~ww xY wt'          |||
          }		 t          t)          |                    }|rt          |	|           n3# t          $ r&}t                              d||           Y d}~nd}~ww xY w|	S )u  Read the last *limit* messages for *session_key*.

    Two-tier read for forward/backward compatibility across Hermes builds:
      1. SQLite (``~/.hermes/state.db`` via :class:`hermes_state.SessionDB`)
         — the canonical store since "spec 002".
      2. Legacy per-session ``<session_id>.jsonl`` transcript file — used when
         SessionDB is unavailable (older builds) or yields no rows.
    In both cases the ``session_id`` comes from the surviving ``sessions.json``
    index.
    z9No sessions.json entry for session_key=%s sessions_dir=%sr]   r^   r,   z*Entry for session_key=%s has no session_idNT)include_ancestorsz<Failed to read messages from SessionDB for session_id=%s: %sr   r   z)Usage attach failed for session_id=%s: %sz:SessionDB returned no rows for session_id=%s; trying JSONLzYNo history found (SQLite empty/unavailable and no JSONL) for session_key=%s session_id=%sz+Falling back to legacy JSONL transcript: %srC   rD   rE   z Failed to read transcript %s: %sz)Usage attach failed for transcript=%s: %s)r[   rN   rf   r?   r-   rg   get_messages_as_conversationre   _normalize_db_messagesr9   r;   r=   r   r   rO   rv   rG   readrM   _parse_transcriptr   )rT   r@   r   r   rY   r]   dbraw_messagesrR   r   rP   
usage_pathr   r;   r2   raws                   r(   read_session_historyr   j  s   " "+|<<E G@)>)@)@	
 	
 	
 	<((HEIIk,B,BHbJ C[QQQ	 
		B	~	 
 ::d ;  LL  	  	  	 NNNC    LLLLLL	   	-EY  H
 ;$9$;$;W\\!
-H-H-HII
 /
 ; ;  G-hFFF   ?       
 OH	
 	
 	
 #:|UCCD +	
 	
 	

 	
LL>EEE$g... 	!&&((C	 	 	 	 	 	 	 	 	 	 	 	 	 	 	   94EEE						 !EYGGGH
'(?(E(EFF 	?%h>>> 
 
 
7s	
 	
 	
 	
 	
 	
 	
 	


 Os   B4 4
C&>C!!C&>AE 
FE>>F0H/ H#H/ #H''H/ *H'+H/ /
I!9II!I!7.J& &
K0KKr   c                   g }|                                  D ]}|                                }|s	 t          j        |          }n# t          j        $ r Y Aw xY w|                    d          t          v rb|                    d          dk    r|t          |                    d          t                    r|                    d          n|}|                    d          dk    rt          |          rt          |          }||r|d         dvr|                    |           t          |          |k    r|| d         n|S )	z:Parse a legacy JSONL transcript into HistoryMessage dicts.rz   rx   session_metamessagemodelzdelivery-mirrorNr   r   )
splitlinesr   rH   r   rL   r-   _SKIP_LINE_TYPESrJ   r.   r   r   r   r    )r   r   r   r   r   parsedr   
normalizeds           r(   r   r     s~    H   #$ #$zz|| 		Z%%FF# 	 	 	H	 ::f!111 ::f// (2&**Y2G2G'N'NZfjj###TZ 777000 s## 	&s++
 	F+3HHH
#### #H 5 58UFGG8Cs   AAAr   c                0   g }| D ]r}t          |t                    s|                    d          r.t          |          r>t	          |          }|P|r|d         dvr]|                    |           st          |          |k    r|| d         n|S )a  Normalise SQLite conversation rows into HistoryMessage dicts.

    ``raw_messages`` come from ``SessionDB.get_messages_as_conversation`` in the
    OpenAI ``{role, content, tool_calls?, ...}`` shape (content already decoded
    and sanitised).  We reuse :func:`normalize_message` for file-attachment /
    ``MEDIA:`` link recovery, and drop delivery-mirror echoes (``observed``)
    plus system-injected user turns to match the previous JSONL behaviour.
    observedNrx   r   )rJ   r.   r-   r   r   r   r    )r   r   r   r   r   r   s         r(   r   r   	  s     H $ $#t$$ 	 77: 	 s## 	&s++
 	F+3HHH
#### #H 5 58UFGG8Cr*   c                    t          |           }g }|                                D ]\  }}|s|                    d          p|                    d          pd}|s7|                    d          p|                    d          }|                    d          p|                    d          }|                    ||||d           |                    d	 d
           |S )z/List all sessions from the sessions.json index.r]   r^   r,   
updated_at	updatedAtdisplay_namedisplayName)
sessionKeyr^   r  r  c                 0    |                      d          pdS )Nr  r,   )r-   )xs    r(   <lambda>zlist_sessions.<locals>.<lambda>H  s    aeeK006B r*   T)keyreverse)rS   rW   r-   r   sort)r@   rX   r   r  rY   sidupdateddisplays           r(   list_sessionsr  4  s   |,,EFkkmm  
U 	ii%%E;)?)?E2 	))L))CUYY{-C-C))N++Guyy/G/G "	
 
 	 	 	 	 KK66KEEEMr*   r   )NN)r,   )9__doc__rH   loggingr9   r   pathlibr   typingr   r   r   r   configr	   	getLogger__name__rN   	frozensetr   compiler   r   r"   r~   r)   r5   r?   rS   r[   r_   rb   rc   rg   rv   r   r.   r   r   r   	MULTILINEr   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r   r*   r(   <module>r     s   "   				 				       , , , , , , , , , , , , $ $ $ $ $ $		8	$	$ 9        RZC   2:"   BJ122 3 4S#X+?    DT$sCx.1 d4S>6J    &1s 1 1 1 1 Xc] d38n    6 #' 3- d38n   $ #' 3- c]   2    6 #'&*( ((3-( DcN#( c]	( ( ( (^ 3 c 3    $YT Yd Y Y Y Y 2:;   
8L 3 4S#X+?    -3 -3 - - - -(14 1HTN 1 1 1 1p,S ,S , , , ,3# 3$tCH~"6 3 3 3 3l>M4j>MS#X'>M 
>M >M >M >MB4: $s)    0:S :Xe_ : : : :*5k*K* 
(3-* * * *Z54j5c5 S#X'5 
	5 5 5 56 #'l l l ll3-l 	l
 l 
$Zl l l ld 	-D -D -D	-D -D 	-D
 
$Z-D -D -D -Df 	$D $D $Dt*$D $D 	$D
 
$Z$D $D $D $DV  d      r*   