Ë
    âß²jyz ã            	      ó  — d Z ddlmZ ddlZddlZddlZddlZddlZddlm	Z	m
Z
mZmZmZ ddlmZmZmZmZ ddlmZ ddlmZ ddlmZ dd	lmZmZmZmZmZm Z m!Z!m"Z"  ejF                  d
«      Z$dZ% e& ejN                  de%jQ                  dd«      d   jQ                  dd«      d   «      «      h d£z  Z)dZ*dZ+dZ,dZ-de-z   Z.dZ/dZ0dZ1dZ2dZ3dZ4dZ5dZ6 e7«       Z8d'd„Z9d(d„Z:d)d „Z;d*d!„Z<d+d,d"„Z=d-d#„Z>d.d$„Z? G d%„ d&«      Z@y)/zÑTranscript persistence for SessionDB: message append/replace/rewind, reactions, resume assembly,
replayed-user dedupe. Mixin bound via the MRO, built on SessionDB's _read_ctx/_execute_write/_read_* primitives.é    )ÚannotationsN)ÚAnyÚDictÚListÚOptionalÚTuple)Ú_DB_PERSISTED_MARKERÚ_is_checkpoint_itemÚ_newest_checkpoint_carrierÚsplit_user_originated_turn)Úsanitize_context)Ú_sanitize_surrogates©Úcoerce_epoch)Ú_COMPRESSION_LOCK_ROW_SQLÚ_ENDED_ROW_SQLÚ_RESET_END_REASONSÚ_RESET_END_REASONS_SQLÚ_ended_by_compressionÚ_legacy_reset_child_sqlÚ_placeholdersÚ_sql_json_extractÚhermes_statea  INSERT INTO messages (session_id, role, content, tool_call_id,
                   tool_calls, tool_name, effect_disposition, timestamp, token_count, finish_reason,
                   reasoning, reasoning_content, reasoning_details, codex_reasoning_items,
                   codex_message_items, platform_message_id, observed, _compressed_summary, active, api_content, display_kind,
                   display_metadata, display_identity)
                   VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)z\w+ú(é   ú)>   ÚidÚ	compactedÚdisplay_orderzý
            INSERT INTO conversation_generations (source, session_key, generation)
            VALUES (?, ?, 1)
            ON CONFLICT(source, session_key) DO UPDATE
                SET generation = conversation_generations.generation + 1
            zLSELECT holder, expires_at FROM session_turn_leases WHERE conversation_id = ?zADELETE FROM compression_locks WHERE session_id = ? AND holder = ?z" AND (active = 1 OR compacted = 1)zLSELECT display_metadata FROM messages WHERE id = ? AND session_id IN ({ids})zGSELECT id FROM messages WHERE session_id = ? AND active = 1 ORDER BY idzxSELECT id, role, content, tool_call_id, tool_calls FROM messages WHERE session_id = ? AND active = 1 ORDER BY id LIMIT ?z:UPDATE sessions SET message_count = ?, tool_call_count = ?zGUPDATE sessions SET message_count = 0, tool_call_count = 0 WHERE id = ?z5UPDATE messages SET display_metadata = ? WHERE id = ?zQUPDATE messages SET active = 0, compacted = 1 WHERE session_id = ? AND active = 1z¤SELECT id, codex_reasoning_items FROM messages WHERE session_id = ? AND active = 1 AND role = 'assistant' AND id < ? AND codex_reasoning_items LIKE '%"compaction"%'z:UPDATE messages SET codex_reasoning_items = ? WHERE id = ?c                óš   — 	 t        j                  | «      S # t         j                  t        f$ r t        j                  |«       |cY S w xY w)zD``json.loads(raw)``; on failure log *warning* and return *fallback*.)ÚjsonÚloadsÚJSONDecodeErrorÚ	TypeErrorÚloggerÚwarning)ÚrawÚfallbackr&   s      ú:/home/ubuntu/.hermes/hermes-agent/hermes_state_messages.pyÚ_json_orr*   >   s@   € ðÜ�z‰z˜#‹ÐøÜ× Ñ ¤)Ð,ò Ü�‰�wÔØŠðús   ‚ —0A
Á	A
c                ó6   — t        | d¬«      }|€|S |dk(  rdS |S )uÆ   Explicit message timestamp (datetime or number) or *default* when invalid or outside the sane
    epoch window â€” the write-side twin of the readers' ``coerce_epoch``: a bad row is never persisted.zmessage timestamp)Úfieldg        r   )ÚvalueÚdefaultÚresults      r)   Ú_coerce_timestampr0   G   s.   € ô ˜%Ð':Ô;€FØ€~Øˆð ˜C’-ˆ3Ð+ VÐ+ó    c                ó”   — t        | t        «      s| S 	 t        j                  | «      S # t        j                  t
        f$ r g cY S w xY w)zktool_calls is a list (live agent) or JSON string (import/export); parse so json.dumps never double-encodes.)Ú
isinstanceÚstrr!   r"   r#   r$   ©Ú
tool_callss    r)   Ú_parse_tool_callsr7   S   sE   € ä�j¤#Ô&ØÐðÜ�z‰z˜*Ó%Ð%øÜ× Ñ ¤)Ð,ò ØŠ	ðús   ”) ©AÁAc                óD   — | €dS t        | t        «      rt        | «      S dS )Nr   r   )r3   ÚlistÚlenr5   s    r)   Ú_tool_calls_countr;   ]   s&   € ØÐ"ˆ1Ð`¼JÀzÔSWÔ<X¬¨Z«Ð`Ð^_Ð`r1   c                óZ   — t        | «      }t        |t        «      rt        |«      S |r|S dS )z]Count of a stored ``tool_calls`` column: list length, *scalar* for a truthy non-list, else 0.r   )r7   r3   r9   r:   )r'   ÚscalarÚparseds      r)   Ú_tool_calls_lenr?   a   s+   € ä˜sÓ#€FÜ$ V¬TÔ2Œ3ˆv‹;ÐQÁ6¸ÐQÈqÐQr1   c                ó<   — t        | t        «      rt        | «      S | S )zPLone surrogates make sqlite3 raise UnicodeEncodeError and abort the whole write.)r3   r4   r   ©r-   s    r)   Ú_scrub_surrogatesrB   g   s   € ä*4°U¼CÔ*@Ô Ó&ÐKÀeÐKr1   c                óJ   — ddl m} t        | d   «      |k  xs  || d   «      S )zJA lock/lease row whose holder is expired or a provably dead local process.r   )Ú(_compression_lock_holder_process_is_deadÚ
expires_atÚholder)r   rD   Úfloat)ÚrowÚnowrD   s      r)   Ú_stale_holderrJ   l   s,   € åEÜ��\Ñ"Ó# sÑ*ÒeÑ.VÐWZÐ[cÑWdÓ.eÐer1   c                  ó¢  — e Zd ZdZdYd„ZedZd„«       ZedZd„«       Zed[d„«       Z	ed\d„«       Z
ed]d„«       Z	 	 	 d^	 	 	 	 	 	 	 	 	 	 	 d_d
„Z	 	 	 	 	 	 d`d„Zedad„«       Z	 	 	 	 	 	 	 	 db	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dcd„Zddd„Z	 	 	 de	 	 	 	 	 	 	 	 	 	 	 	 	 dfd„Zddœ	 	 	 	 	 dgd„Zdhd„Zddœ	 	 	 did„Zdjd„Zdkd„Zddœdld„Zddddœ	 	 	 dmd„Zdnd„Zdod„Zdpd„Zdqd „Z	 	 dr	 	 	 	 	 dsd!„Zedtd"„«       Zdud#„Zedvd$„«       Z dwd%„Z!dxd&„Z"dyd'„Z#dd(œdzd)„Z$	 	 d{	 	 	 	 	 	 	 	 	 d|d*„Z%d}d+„Z&d~d,„Z'	 	 	 	 	 	 	 	 	 	 dd-„Z(d€d.„Z)d�d/„Z*ed‚d0„«       Z+d1„ Z,dwd2„Z-	 	 	 	 dƒd3„Z.d„d4„Z/ed…d5„«       Z0	 	 	 d†	 	 	 	 	 	 	 	 	 d‡d6„Z1dˆd7„Z2d‰dŠd8„Z3d‹d9„Z4dŒd:„Z5	 	 	 	 d�	 	 	 	 	 	 	 	 	 dŽd;„Z6d�d<„Z7d	d	d=œ	 	 	 	 	 d�d>„Z8d‘d?„Z9d’d@„Z:d“dA„Z;d	dBœd”dC„Z<d•d	dBœd–dD„Z=d—dE„Z>d‹dF„Z?ed˜dG„«       Z@ed™dH„«       ZAe	 	 	 	 dšdI„«       ZBd›dJ„ZCedœdK„«       ZDd�dL„ZEd	dddMœ	 	 	 	 	 dždN„ZFd•dxdO„ZGdŸdP„ZHd dQ„ZId	dRœd¡dS„ZJdwdT„ZKd¢dU„ZLd£dV„ZMd	ddWœd¤dX„ZNy)¥ÚSessionMessagesMixinzNMessage append/replace/rewind, reactions, resume conversations, replay dedupe.c                ó¸   ‡— |t         vry|j                  d|f«      j                  «       Š‰€yˆfd„dD «       \  }}|r|r|j                  t        ||f«       yyy)a'  Advance the peer's conversation generation past a boundary, in the txn that writes it. Only
        ``_RESET_END_REASONS`` count (compression continues one conversation). Never derived from session
        rows (deletes/prunes could re-emit a retired affinity identity); it only ever increments.Nz5SELECT source, session_key FROM sessions WHERE id = ?c              3  ó^   •K  — | ]$  }t        ‰|   xs d «      j                  «       –— Œ& y­w)Ú N)r4   Ústrip)Ú.0ÚkrH   s     €r)   ú	<genexpr>zESessionMessagesMixin._bump_conversation_generation.<locals>.<genexpr>~   s'   øè ø€ Ò\¸Qœs 3 q¡6¢<¨RÓ0×6Ñ6×8Ñ\ùs   ƒ*-)ÚsourceÚsession_key)r   ÚexecuteÚfetchoneÚ_BUMP_GENERATION_SQL)ÚselfÚconnÚ
session_idÚ
end_reasonrT   rU   rH   s         @r)   Ú_bump_conversation_generationz2SessionMessagesMixin._bump_conversation_generationu   sg   ø€ ð Ô/Ñ/ØØ�l‰lÐRÐU_ÐTaÓb×kÑkÓmˆØˆ;ØÛ\ÐB[Ô\Ñˆ�Ù‘kØ�L‰LÔ-°¸Ð/DÕEð "ˆ6r1   c                ó  — t        |t        «      rt        |«      S |�t        |t        t        t
        f«      r|S 	 | j                  t        j                  |«      z   S # t        t        f$ r t        t        |«      «      cY S w xY w)aS  Serialize list/dict content (multimodal parts) as a sentinel-prefixed JSON string (sqlite3 binds
        only scalars). Lone UTF-16 surrogates (unsanitized web-scraped tool results) are scrubbed here: left
        raw, sqlite3 raises UnicodeEncodeError and the session silently stops persisting. Pairs with
        :meth:`_decode_content`.)r3   r4   r   ÚbytesÚintrG   Ú_CONTENT_JSON_PREFIXr!   Údumpsr$   Ú
ValueError©ÚclsÚcontents     r)   Ú_encode_contentz$SessionMessagesMixin._encode_content‚   su   € ô �gœsÔ#Ü'¨Ó0Ð0Øˆ?œj¨´5¼#¼uÐ2EÔFØˆNð	6Ø×+Ñ+¬d¯j©j¸Ó.AÑAÐAøÜœ:Ð&ò 	6Ü'¬¨G«Ó5Ò5ð	6ús   ¼!A Á#BÂBc                ó¢   — t        |t        «      r>|j                  | j                  «      r#t	        |t        | j                  «      d |d«      S |S )z;Reverse :meth:`_encode_content`; returns scalars unchanged.NzCFailed to decode JSON-encoded message content; returning raw string)r3   r4   Ú
startswithra   r*   r:   rd   s     r)   Ú_decode_contentz$SessionMessagesMixin._decode_content‘   sO   € ô �gœsÔ#¨×(:Ñ(:¸3×;SÑ;SÔ(TÜ˜G¤C¨×(@Ñ(@Ó$AÐ$BÐCÀWØUóWð Wàˆr1   c                óF  — | syt        | t        «      r@t        | t        d«      } | t        u ryt        | t        «      sPt
        j                  d«       yt        | t        «      s*t
        j                  dt        | «      j                  «       yt        j                  | «      S )z‹Serialize ``display_metadata`` for its TEXT column; an already-serialized JSON string
        (import/replace paths) is not double-encoded.Nz+Ignoring non-JSON display metadata on writez-Ignoring non-object display metadata on writez6Ignoring unexpected display metadata type on write: %s)r3   r4   r*   Ú_INVALIDÚdictr%   r&   ÚtypeÚ__name__r!   rb   ©Údisplay_metadatas    r)   Ú_encode_display_metadataz-SessionMessagesMixin._encode_display_metadata™   s†   € ñ  ØÜÐ&¬Ô,Ü'Ð(8¼(ÐDqÓrÐØ¤8Ñ+ØÜÐ.´Ô5Ü—‘ÐNÔOØÜÐ,¬dÔ3Ü�N‰NÐSÔUYÐZjÓUk×UtÑUtÔuØÜ�z‰zÐ*Ó+Ð+r1   c                óÒ   — | €y| }t        d«      D ]$  }t        |t        «      sŒt        |t        d«      }Œ& |t        u ryt        |t
        «      st        j                  d«       y|S )z½Decode a ``display_metadata`` column to a dict (never raw TEXT: the desktop does ``'task_count'
        in meta``). Pre-guard rows are double-encoded, so a second string layer is unwrapped.Né   z0Ignoring invalid display metadata on message rowz3Ignoring non-object display metadata on message row)Úranger3   r4   r*   rl   rm   r%   r&   )r'   ÚmetaÚ_s      r)   Ú_decode_display_metadataz-SessionMessagesMixin._decode_display_metadata«   sk   € ð ˆ;ØØˆÜ�q“ò 	dˆAÜ˜$¤Õ$Ü ¤hÐ0bÓc‘ð	dð ”8ÑØÜ˜$¤Ô%Ü�N‰NÐPÔQØØˆr1   c                óX   — | sdS t        | t        «      r| S t        j                  | «      S )a  Serialize a structured reasoning field for its TEXT column. Strings are stored as-is: round-trips
        (get_messages -> replace_messages) hand back raw TEXT; re-dumping would double-encode it and
        reasoning-replay consumers (``isinstance(..., list)``) would drop it.N)r3   r4   r!   rb   rA   s    r)   Ú_reasoning_json_textz)SessionMessagesMixin._reasoning_json_text¼   s)   € ñ
 !ˆtÐ^´
¸5Ä#Ô0F uÐ^ÌDÏJÉJÐW\ÓL]Ð^r1   NFc	           
     ó&  — ddl m}	 ddlm}
m} |ru|j                  t        |f«      j                  «       }|�Nt        |t        j                  «       «      r|j                  t        ||d   f«       n|d   |k7  r |	d|›d�«      ‚|s|rÏ| j                  ||«      }|j                  t        |f«      j                  «       }t        j                  «       }|rR|�|d   |k7  r |d|›�«      ‚t        |d	   «      |k  r\|j                  d
|t        dt        |«      «      z   ||f«       n0|�.t        ||«      s |d|›�«      ‚|j                  d||d   f«       t        |j                  t         |f«      j                  «       «      r|s |
|«      ‚yy)u[  Transcript-write admission checks, run INSIDE the write txn by every writer. Ordinary appends do
        NOT check compression_locks: the lock only stops two COMPRESSIONS colliding and archive_and_compact()
        commits against a watermark, so concurrent appends are safe (blocking them killed turns during slow
        summaries). Destructive user mutations opt in via ``reject_active_*`` so a compressor that captured
        its watermark cannot resurrect the removed turn.

        Shared by :meth:`append_message` and :meth:`append_messages_batch` so the two writers can never
        diverge on these correctness invariants (this guard has already needed targeted fixes â€” see the
        #74478 patience note below). User-initiated transcript mutations may opt in to rejecting an active
        unowned turn lease in that same transaction.
        r   ©Ú!SessionCompressionInProgressError)ÚCompressionSessionClosedErrorÚSessionTurnLeaseLostErrorNrF   zSession z& is being compressed by another writerz7Session turn lease lost; refusing transcript write for rE   zVUPDATE session_turn_leases SET expires_at = ? WHERE conversation_id = ? AND holder = ?gš™™™™™¹?zCSession has an active turn lease; refusing transcript mutation for zHDELETE FROM session_turn_leases WHERE conversation_id = ? AND holder = ?)r   r}   Úhermes_state_errorsr~   r   rV   r   rW   rJ   ÚtimeÚ_DELETE_COMPRESSION_LOCK_SQLÚ_session_turn_lease_key_on_connÚ_TURN_LEASE_ROW_SQLrG   Úmaxr   r   )rY   rZ   r[   Úcompression_lock_holderÚturn_lease_holderÚturn_lease_ttl_secondsÚreject_active_turn_leaseÚreject_active_compression_lockÚallow_closed_compression_parentr}   r~   r   Úactive_lockÚconversation_idÚleaserI   s                   r)   Ú_check_transcript_write_guardsz3SessionMessagesMixin._check_transcript_write_guardsÃ   s½  € õ 	Cß`ñ *ØŸ,™,Ô'@À:À-ÓP×YÑYÓ[ˆKØÐ&Ü  ¬d¯i©i«kÔ:Ø—L‘LÔ!=À
ÈKÐX`ÑLaÐ?bÕcØ  Ñ*Ð.EÒEÙ;Ø" : .Ð0VÐWóYð YáÑ 8Ø"×BÑBÀ4ÈÓTˆOØ—L‘LÔ!4°Ð6HÓI×RÑRÓTˆEÜ—)‘)“+ˆCÙ Ø�= E¨(¡OÐ7HÒ$HÙ3ØQÐR\ÐQ_Ð`óbð bä˜˜|Ñ,Ó-°Ò4ð —L‘Lð "Càœs 3¬Ð.DÓ(EÓFÑFÈÐYjÐkõmð Ð"Ü$ U¨CÔ0Ù3Ø]Ð^hÐ]kÐlónð nð —‘ÐgØ$ e¨H¡oÐ6ô8ä  §¡¬n¸z¸mÓ!L×!UÑ!UÓ!WÔXñ  bAÙ/°
Ó;Ð;ð bAÐXr1   c               ó~  ‡ ‡‡‡— d„ }ˆˆfd„Š‰ j                  ‰j                  d«      «      }|rt        j                  |«      nd}	t	        ‰j                  d«      «      }
‰ j                  ‰j                  d«      «      }|||‰j                  d«      |	|
‰j                  d«      |d	œ}|||‰j                  d«      |	|
‰j                  d
«      |‰j                  d«      ‰j                  d«      t	         ‰d«      «      t	         ‰d«      «      gˆˆ fd„dD «       ¢‰j                  d«      xs ‰j                  d«      ‘‰j                  d«      rdnd‘‰j                  d«      rdnd‘d‘ |‰j                  d«      «      ‘ |‰j                  d«      «      ‘|‘‰ j                  ‰ j                  |«      «      ‘­S )zûBind values for ``_INSERT_MESSAGE_SQL`` from one message dict (*tool_calls* already parsed;
        *keep_reasoning* False NULLs every reasoning column). ``platform_message_id`` falls back to
        ``message_id`` (yuanbao's message-dict convention).c                ó<   — t        | t        «      rt        | «      S d S ©N)r3   r4   rB   )Úvs    r)   ú<lambda>z:SessionMessagesMixin._message_row_params.<locals>.<lambda>  s   € ¼ÀAÄsÔ9KÔ!2°1Ó!5€ ÐQU€ r1   c                ó.   •— ‰r‰j                  | «      S d S r’   ©Úget)ÚkeyÚkeep_reasoningÚmsgs    €€r)   r”   z:SessionMessagesMixin._message_row_params.<locals>.<lambda>  s   ø€ ± §¡¨£€ ÀT€ r1   rf   NÚ	tool_namerq   Útool_call_idÚdisplay_kind)Úrolerf   Ú	timestamprœ   r6   r›   r�   rq   Úeffect_dispositionÚtoken_countÚfinish_reasonÚ	reasoningÚreasoning_contentc              3  óL   •K  — | ]  }‰j                   ‰|«      «      –— Œ y ­wr’   )rz   )rQ   rR   Ú
_reasoningrY   s     €€r)   rS   z;SessionMessagesMixin._message_row_params.<locals>.<genexpr>  s*   øè ø€ ò ^Øð ×'Ñ'©
°1«×6ñ ^ùs   ƒ!$©Úreasoning_detailsÚcodex_reasoning_itemsÚcodex_message_itemsÚplatform_message_idÚ
message_idÚobservedr   r   Ú_compressed_summaryÚapi_content)rg   r—   r!   rb   rB   rr   Ú_display_identityÚ_display_dedupe_key)rY   r[   rž   rš   r6   Úmessage_timestampr™   Ú_str_or_noneÚencoded_contentÚencoded_tool_callsÚencoded_tool_namerq   Úidentity_rowr¦   s   `  `  `      @r)   Ú_message_row_paramsz(SessionMessagesMixin._message_row_paramsü   sÚ  û€ ñ
 VˆÜIˆ
Ø×.Ñ.¨s¯w©w°yÓ/AÓBˆÙ7AœTŸZ™Z¨
Ô3ÀtÐÜ-¨c¯g©g°kÓ.BÓCÐØ×8Ñ8¸¿¹ÐASÓ9TÓUÐà _ÐCTØŸG™G NÓ3ÐCUØ*¸C¿G¹GÀNÓ<SØ 0ñ	
ˆð ˜D /°3·7±7¸>Ó3JØÐ 1Ø�G‰GÐ(Ó)Ð+<¸c¿g¹gÀmÓ>TÐVY×V]ÑV]Ð^mÓVnÜ™j¨Ó5Ó6Ô8IÉ*ÐUhÓJiÓ8jð	^ô^Ø\ô^ð		^ð �G‰GÐ)Ó*ÒC¨c¯g©g°lÓ.Cð	^ð —‘˜Ô$‰A¨!ð	^ð 36·'±'Ð:OÔ2P©QÐVWð	^ð Z[ð	^ñ ˜Ÿ™ Ó/Ó0ð	^ñ 3?¸s¿w¹wÀ~Ó?VÓ2Wð	^ð ð	^ð #×4Ñ4°T×5MÑ5MÈlÓ5[Ó\ñ	^ð 		^r1   c               ó¤   — |rdnd|ff\  }}|dkD  r| j                  d|› d�g |¢|‘|‘­«       y|dkD  r| j                  d|› d�g |¢|‘­«       yy)zXBump sessions.* counters after an insert; *unit* bakes the ``+ 1`` literal into the SQL.)Ú1© ú?r   z4UPDATE sessions SET message_count = message_count + zK,
                       tool_call_count = tool_call_count + ? WHERE id = ?ú WHERE id = ?N)rV   )rZ   r[   Úinsertedr6   ÚunitÚincÚparamss          r)   Ú_bump_session_countersz+SessionMessagesMixin._bump_session_counters  s‡   € ñ $(‘i¨c°H°;Ð-?‰ˆˆVØ˜Š>Ø�L‰LØHÈÈð NJð Mà1�&Ð1˜*Ð1 jÑ1õ3ð ˜Š\Ø�L‰LØFÀsÀeÈ=ÐYÐ[pÐ]cÐ[pÐeoÑ[põrð r1   c                ó.  ‡ ‡‡‡‡‡‡— t        t        «       «      }‰ j                  |«      |d<   t        ‰«      Št	        |t        j
                  «       «      }‰ j                  ‰||‰|d¬«      Šˆˆˆ ˆˆˆˆfd„}‰ j                  |‰ j                  ¬«      S )a  Append one message; returns the row id and bumps the session counters. ``platform_message_id``:
        the platform's own id. ``api_content``: byte-fidelity sidecar, the exact string sent to the API when
        it differed from ``content``, stored as sent except lone surrogates.rq   T©r™   c                ó²   •— ‰j                  | ‰‰‰‰¬«       | j                  t        ‰«      j                  }‰j	                  | ‰dt        ‰«      d¬«       |S )N©r‡   rˆ   r   T©r¿   )r�   rV   Ú_INSERT_MESSAGE_SQLÚ	lastrowidrÂ   r;   )	rZ   Úmsg_idr†   rÁ   rY   r[   r6   r‡   rˆ   s	     €€€€€€€r)   Ú_doz0SessionMessagesMixin.append_message.<locals>._do8  s`   ø€ Ø×/Ñ/°°jÐBYØ"3ÐLbð 0ô dà—\‘\Ô"5°vÓ>×HÑHˆFØ×'Ñ'¨¨j¸!Ô=NÈzÓ=ZÐaeÐ'ÔfØˆMr1   ©Ú
patience_s)	rm   Úlocalsrr   r7   r0   r�   r¸   Ú_execute_writeÚ_TRANSCRIPT_WRITE_PATIENCE_S)rY   r[   rž   rf   r›   r6   rœ   r¡   r¢   r£   r¤   r¨   r©   rª   r«   r­   r    r®   rŸ   r¯   r�   rq   r†   r‡   rˆ   rš   r²   rË   rÁ   s   ``   `                ```   @r)   Úappend_messagez#SessionMessagesMixin.append_message%  s‘   þ€ ô ”6“8‹nˆà"&×"?Ñ"?Ð@PÓ"QˆÐÑÜ& zÓ2ˆ
Ü-¨i¼¿¹»ÓEÐØ×)Ñ)Ø˜˜c :Ð/@ÐQUð *ó Wˆ÷	ò 	ð ×"Ñ" 3°4×3TÑ3TÐ"ÓUÐUr1   c                ó  ‡ ‡‡‡‡— ‰j                  d«      Š‰st        d«      ‚|‰j                  d«      rdnd‰dœ}‰ j                  ‰d|dt        j                  «       d	¬
«      Šˆˆˆˆ ˆfd„}‰ j	                  |‰ j
                  ¬«      S )a  Record a detached API result once, between client turns, including replay after rotation.

        The event's unit id, not its text or active flag, is the identity. Check and insert
        share the writer transaction, so independent gateway processes cannot duplicate it.
        Údelegation_idz3Delegation delivery requires a stable delegation_idÚpresentation_suppressedÚhiddenÚasync_delegation_complete)rf   r�   rq   ÚuserNTrÄ   c           	     ó  •— | j                  d‰‰‰j                  dd«      f«      j                  «       }|�|d   S ‰j                  | ‰d d¬«       | j                  t        ‰«      j
                  }‰j                  | ‰ddd¬«       |S )	NaX  WITH RECURSIVE lineage(id) AS (
                    SELECT ? UNION
                    SELECT s.parent_session_id FROM sessions s JOIN lineage l ON s.id = l.id
                    JOIN sessions p ON p.id = s.parent_session_id WHERE p.end_reason = 'compression'
                ) SELECT m.id FROM messages m JOIN lineage l ON m.session_id = l.id
                WHERE m.display_kind IN ('async_delegation_complete', 'hidden')
                AND json_extract(m.display_metadata, '$.delegation_id') = ?
                AND coalesce(json_extract(m.display_metadata, '$.delivery_notice'), '') = ? LIMIT 1Údelivery_noticerO   r   T)r‰   r   rÇ   )rV   r—   rW   r�   rÈ   rÉ   rÂ   )rZ   ÚexistingrÊ   rÓ   ÚmetadatarÁ   rY   r[   s      €€€€€r)   rË   z<SessionMessagesMixin.append_delegation_delivery.<locals>._doP  sŸ   ø€ Ø—|‘|ðgð ˜]¨H¯L©LÐ9JÈBÓ,OÐPó	R÷ S[ÑRZÓR\ð ð Ð#Ø ‘{Ð"Ø×/Ñ/°°jÀ$ÐaeÐ/ÔfØ—\‘\Ô"5°vÓ>×HÑHˆFØ×'Ñ'¨¨j¸!¸QÀTÐ'ÔJØˆMr1   rÌ   )r—   rc   r¸   r�   rÏ   rÐ   )rY   r[   rf   rÛ   rš   rË   rÓ   rÁ   s   `` `  @@r)   Úappend_delegation_deliveryz/SessionMessagesMixin.append_delegation_deliveryB  s�   ü€ ð !Ÿ™ _Ó5ˆÙÜÐRÓSÐSØ!Ø+3¯<©<Ð8QÔ+R™xÐXsØ#+ñ-ˆð ×)Ñ)¨*°f¸cÀ4ÌÏÉËÐeiÐ)Ójˆ÷	ð 	ð$ ×"Ñ" 3°4×3TÑ3TÐ"ÓUÐUr1   c           	     óà   ‡ ‡‡‡‡‡‡— ‰sy‰�<t        ‰«      ‰kD  r.t        ˆˆˆˆ ˆˆˆfd„t        dt        ‰«      ‰«      D «       «      S ˆˆˆ ˆˆˆfd„}‰ j                  |‰ j                  ¬«      S )zÊAppend *messages* in ONE write txn (all rows land or none, guards run once); returns the inserted
        count. ``chunk_rows`` bounds txn size for LARGE copies (branch seeds; FTS triggers run per row).r   c              3  óV   •K  — | ]   }‰j                  ‰‰||‰z    ‰‰‰¬ «      –— Œ" y­w))r†   r‡   rˆ   N)Úappend_messages_batch)	rQ   ÚstartÚ
chunk_rowsr†   ÚmessagesrY   r[   r‡   rˆ   s	     €€€€€€€r)   rS   z=SessionMessagesMixin.append_messages_batch.<locals>.<genexpr>m  sH   øè ø€ ò Bð ð ×1Ñ1°*¸hÀuÈUÐU_ÑM_Ð>`Ø,CÐWhØ+Að 2÷ Cñ Bùs   ƒ&)c                óÚ   •— ‰j                  | ‰‰‰	‰
¬«       ddlm}  || ‰‰‰j                  ‰j                  ¬«      }‰j                  | ‰|«      \  }}‰j                  | ‰||d¬«       |S )NrÆ   r   )Ú#resolve_and_repair_transcript_batch)Úencode_content_fnÚdecode_content_fnFrÇ   )r�   Úagent.transcript_repairrä   rg   rj   Ú_insert_message_rowsrÂ   )rZ   rä   Úinserted_rowsr¾   Útool_calls_totalr†   râ   rY   r[   r‡   rˆ   s        €€€€€€r)   rË   z7SessionMessagesMixin.append_messages_batch.<locals>._doq  s…   ø€ Ø×/Ñ/°°jÐBYØ"3ÐLbð 0ô dåSÙ?ÀÀjÐRZØ"&×"6Ñ"6È$×J^ÑJ^ô`ˆMà)-×)BÑ)BÀ4ÈÐUbÓ)cÑ&ˆHÐ&Ø×'Ñ'¨¨j¸(ÐDTÐ[`Ð'ÔaØˆOr1   rÌ   )r:   Úsumru   rÏ   rÐ   )rY   r[   râ   r†   r‡   rá   rˆ   rË   s   ``````` r)   rß   z*SessionMessagesMixin.append_messages_batchd  sw   þ€ ñ ØØÐ!¤c¨(£m°jÒ&@Ü÷ Bñ Bô # 1¤c¨(£m°ZÓ@ôBó Bð B÷	ñ 	ð ×"Ñ" 3°4×3TÑ3TÐ"ÓUÐUr1   rp   c               óR   ‡ ‡‡‡‡‡— ‰r‰r‰syˆˆˆˆˆ ˆfd„}‰ j                  |«      S )a  Stamp presentation metadata on this turn's freshly persisted row (newest active row by content,
        right after the serial turn flushed); the model still sees ``role``/``content`` unchanged, so
        producer provenance survives without classifying by content at render time.Fc                óÐ   •— | j                  d‰‰‰j                  ‰«      f«      j                  «       }|€y| j                  dt        ‰«      ‰j	                  ‰«      |d   f«       y)NzqSELECT id FROM messages WHERE session_id = ? AND role = ? AND content = ? AND active = 1 ORDER BY id DESC LIMIT 1FzGUPDATE messages SET display_kind = ?, display_metadata = ? WHERE id = ?r   T)rV   rg   rW   rB   rr   )rZ   rH   rf   r�   rq   rž   rY   r[   s     €€€€€€r)   rË   zJSessionMessagesMixin.set_latest_matching_message_display_kind.<locals>._do„  sv   ø€ Ø—,‘,ð  Jà˜T 4×#7Ñ#7¸Ó#@ÐAóCçCKÁ8Ã:ð ð ˆ{ØØ�L‰LÐbÜ" <Ó0°$×2OÑ2OÐP`Ó2aÐcfÐghÑciÐjôlàr1   ©rÏ   )rY   r[   rž   rf   r�   rq   rË   s   `````` r)   Ú(set_latest_matching_message_display_kindz=SessionMessagesMixin.set_latest_matching_message_display_kind|  s,   ý€ ñ ¡±Ø÷	ñ 	ð ×"Ñ" 3Ó'Ð'r1   c                ó°   — |xs i j                  | j                  «      }t        |t        «      r!|D �cg c]  }t        |t        «      sŒ|‘Œ c}S g S c c}w )zEWell-formed (dict) reactions stored under ``REACTIONS_METADATA_KEY``.)r—   ÚREACTIONS_METADATA_KEYr3   r9   rm   )rY   rv   Ú	reactionsÚrs       r)   Ú_reaction_listz#SessionMessagesMixin._reaction_list�  sI   € à’Z˜R×$Ñ$ T×%@Ñ%@ÓAˆ	Ü@JÈ9ÔVZÔ@[˜9Ö<�a¬
°1´dÕ(;’Ò<ÐcÐacÐcùÒ<s   ´AÁ
Ar×   )Úauthorc               óx   ‡ ‡‡‡‡‡— |r‰€y‰ j                  |‰«      \  ŠŠˆˆˆˆˆ ˆfd„}‰ j                  |«      S )a*  Set (``emoji=None``: clear) *author*'s reaction. Tapback semantics: one per author per message;
        the same emoji again clears, a different one replaces. Returns the list after the write, or
        ``None`` for a row outside the session's visible resume lineage (see ``_reaction_row_query``).Nc                ó^  •— | j                  ‰‰
«      j                  «       }|€y ‰j                  |d   «      xs i }‰j                  |«      }|D �cg c]  }|j	                  d«      ‰k7  sŒ|‘Œ }}t        ˆfd„|D «       d «      }‰rF|�|j	                  d«      ‰k7  r0|j                  t        ‰«      ‰t        j                  «       dœ«       |r||‰j                  <   n|j                  ‰j                  d «       | j                  t        |r‰j                  |«      nd ‰	f«       |S c c}w )Nr   rõ   c              3  óL   •K  — | ]  }|j                  d «      ‰k(  sŒ|–— Œ y­w)rõ   Nr–   )rQ   ró   rõ   s     €r)   rS   zISessionMessagesMixin.set_message_reaction.<locals>._do.<locals>.<genexpr>£  s    øè ø€ ÒN 1°A·E±E¸(³OÀvÓ4MœQÑNùs   ƒ$�$Úemoji)rù   rõ   Úat)rV   rW   rx   rô   r—   ÚnextÚappendrB   r�   rñ   ÚpopÚ_SET_DISPLAY_META_SQLrr   )rZ   rH   rv   rÚ   ró   rò   Úpreviousrõ   rù   Úmessage_row_idrÁ   rY   Úsqls          €€€€€€r)   rË   z6SessionMessagesMixin.set_message_reaction.<locals>._doœ  s  ø€ Ø—,‘,˜s FÓ+×4Ñ4Ó6ˆCØˆ{ØØ×0Ñ0°°Q±Ó8Ò>¸BˆDØ×*Ñ*¨4Ó0ˆHØ$,ÖJ˜q°·±°h³À6Ó0IšÐJˆIÐJÜÓN¨ÔNÐPTÓUˆHÙ˜(Ð*¨h¯l©l¸7Ó.CÀuÒ.LØ× Ñ Ô+<¸UÓ+CÈvÔ]a×]fÑ]fÓ]hÑ!iÔjÙØ4=��T×0Ñ0Ò1à—‘˜×4Ñ4°dÔ;Ø�L‰LÔ.ÑX\°×1NÑ1NÈtÔ1TÐbfÐhvÐ0wÔxØÐùò Ks   ÁD*Á,D*)Ú_reaction_row_queryrÏ   )rY   r[   r   rù   rõ   rË   rÁ   r  s   ` ``` @@r)   Úset_message_reactionz)SessionMessagesMixin.set_message_reaction”  sC   ý€ ñ
 ˜^Ð3ØØ×.Ñ.¨z¸>ÓJ‰ˆˆV÷	ñ 	ð  ×"Ñ" 3Ó'Ð'r1   c                óš   — |r|€g S  | j                   | j                  ||«      Ž }|�#| j                  | j                  |d   «      «      S g S )z<Reaction list persisted on one message row (never ``None``).r   )Ú	_read_oner  rô   rx   )rY   r[   r   rH   s       r)   Úget_message_reactionsz*SessionMessagesMixin.get_message_reactions®  sV   € á˜^Ð3ØˆIØˆd�n‰n˜d×6Ñ6°zÀ>ÓRÐSˆØMPÈ_ˆt×"Ñ" 4×#@Ñ#@ÀÀQÁÓ#HÓIÐdÐbdÐdr1   c                ón   — | j                  |«      }t        j                  t        |«      ¬«      |g|¢­fS )un  A reaction addresses a row the client can SEE, and a display resume materializes the whole
        compression lineage (active + compacted rows, with row ids) â€” so a row is "in this session" when
        its owner is any lineage segment, not only the tip, and a rewound row is not. Explicit ``/branch``
        copies keep their own rows (``_resume_lineage_ids``).)Úids)Ú_resume_lineage_idsÚ_DISPLAY_META_ROW_SQLÚformatr   )rY   r[   r   Úlineages       r)   r  z(SessionMessagesMixin._reaction_row_queryµ  s;   € ð
 ×*Ñ*¨:Ó6ˆÜ$×+Ñ+´¸gÓ0FÐ+ÓGÈ.ÐIcÐ[bÑIcÐcÐcr1   c               ób   ‡ ‡‡— |sg S ‰ j                  |«      Šˆˆˆ fd„}‰ j                  |«      S )a3  Return *author*'s not-yet-surfaced reactions and mark them seen. Reactions are announced on the
        NEXT user turn (never by rewriting the reacted message: cache-safe); ``seen`` makes it exactly once.
        Include compaction-archived history that remains visible, but exclude rewound/superseded rows.c                óà  •— g }| j                  dt        ‰	«      › dt        › dt        dd‰
j                  z   «      › d�t        ‰	«      «      j                  «       D �]  }‰
j                  |d   «      }|r|j                  ‰
j                  «      nd }t        |t        «      sŒHd}|D ]”  }t        |t        «      r%|j                  d«      ‰k7  s|j                  d	«      rŒ9d
|d	<   d
}‰
j                  |d   «      }|j                  |d   |d   |j                  d«      xs dt        |t        «      r|nddœ«       Œ– |sŒæ| j                  t        ‰
j!                  |«      |d   f«       �Œ |S )NzNSELECT id, role, content, display_metadata FROM messages WHERE session_id IN (r   z AND rq   z$.z IS NOT NULL ORDER BY idFrõ   ÚseenTrf   r   rž   rù   rO   )Úrow_idrž   rù   Útext)rV   r   Ú_DISPLAY_ACTIVE_CLAUSEr   rñ   ÚtupleÚfetchallrx   r—   r3   r9   rm   rj   rü   r4   rþ   rr   )rZ   ÚpendingrH   rv   rò   ÚchangedÚreactionrf   rõ   r  rY   s           €€€r)   rË   z7SessionMessagesMixin.take_unseen_reactions.<locals>._doÄ  sŽ  ø€ ØˆGð —|‘|ð %,Ü,9¸'Ó,BÐ+CÀ1ÔE[ÐD\ð ]Ü,Ð-?ÀÈ×HcÑHcÑAcÓdÐeð f"ð%"ô $)¨£>ó3÷ 4<±8³:ój�ð ×4Ñ4°SÐ9KÑ5LÓM�ÙEI˜DŸH™H T×%@Ñ%@ÔAÈt�	Ü! )¬TÔ2ØØ�Ø )ò N�HÜ% h´Ô5¸¿¹ÀhÓ9OÐSYÒ9YÐ]e×]iÑ]iÐjpÔ]qØ Ø'+�H˜VÑ$Ø"�GØ"×2Ñ2°3°y±>ÓB�GØ—N‘NØ"% d¡)°S¸±[È8Ï<É<ÐX_ÓK`ÒKfÐdfÜ+5°g¼sÔ+C¡Èñ$Mõ NðNò Ø—L‘LÔ!6¸×9VÑ9VÐW[Ó9\Ð^aÐbfÑ^gÐ8hÖið'jð( ˆNr1   )r	  rÏ   )rY   r[   rõ   rË   r  s   ` ` @r)   Útake_unseen_reactionsz*SessionMessagesMixin.take_unseen_reactions½  s5   ú€ ñ ØˆIØ×*Ñ*¨:Ó6ˆö	ð2 ×"Ñ" 3Ó'Ð'r1   r   T)rž   ÚoffsetÚrequire_textc               ó|   — |r	|dvs|dk  ry|rdnd}| j                  d|› d�||t        |«      f«      }|r|d   S dS )zÒRow id of the most recent active *role* message, or ``None``. ``offset`` steps back; ``require_text``
        skips rows without plain-text content so "the latest message" never resolves to an invisible bubble.>   r×   Ú	assistantr   Nz0AND content IS NOT NULL AND TRIM(content) != '' rO   zISELECT id FROM messages WHERE session_id = ? AND role = ? AND active = 1 z!ORDER BY id DESC LIMIT 1 OFFSET ?©r  r`   )rY   r[   rž   r  r  Útext_filterrH   s          r)   Úlatest_message_row_idz*SessionMessagesMixin.latest_message_row_idß  se   € ñ ˜TÐ)>Ñ>À&È1Â*ØÙLXÑHÐ^`ˆØ�n‰nð Ø)˜]Ð*KðMà˜œs 6›{Ð+ó-ˆñ ˆs�1‰vÐ& $Ð&r1   c                ó:   — | j                  d|f«      }|r|d   S dS )a  Role of the newest active user/assistant/tool row, or ``None``. ``session_meta`` /
        ``system`` rows are transcript bookkeeping stripped before the model sees history, so
        they must not hide an open user tail from the failed-turn boundary check.z�SELECT role FROM messages WHERE session_id = ? AND active = 1 AND role NOT IN ('session_meta', 'system') ORDER BY id DESC LIMIT 1r   N©r  ©rY   r[   rH   s      r)   Úlatest_conversation_rolez-SessionMessagesMixin.latest_conversation_roleë  s2   € ð �n‰nðRØT^ÐS`óbˆñ ˆs�1‰vÐ& $Ð&r1   c                óT   — |sy| j                  dt        |«      |f«      }|r|d   S dS )zDRole of the active message at *row_id* in *session_id*, or ``None``.NzHSELECT role FROM messages WHERE id = ? AND session_id = ? AND active = 1r   r  )rY   r[   r  rH   s       r)   Úget_message_rolez%SessionMessagesMixin.get_message_roleô  sB   € áØØ�n‰nÐgÔjmÐntÓjuð  xBð  jCó  DˆÙˆs�1‰vÐ& $Ð&r1   c                óN  — t        j                   «       }dx}}|D ]¹  }|j                  dd«      }t        |j                  d«      «      }	t        |j                  d«      |«      }
|j	                  t
        | j                  ||||	|
|dk(  ¬«      «      }|
|d<   |j                  �|j                  |d<   |d	z  }|t        |	«      z  }t        ||
«      d
z   }Œ» t        |d«      }|dk\  r;t        ||   j                  d«      t        «      r| j                  ||||   d   «       ||fS )zÌInsert *messages* as fresh active rows in the caller's txn -> ``(inserted, tool_call_count)``.
        Never touches sessions.* counters (callers reconcile differently); reasoning kept for assistant rows.r   rž   Úunknownr6   rŸ   r  rÄ   Ú_row_idr   g�íµ ÷Æ°>r©   )r�   r—   r7   r0   rV   rÈ   r¸   rÉ   r;   r…   r   r3   r`   Ú_drop_shadowed_checkpoint_rows)rY   rZ   r[   râ   Únow_tsr¾   rê   rš   rž   r6   r²   ÚcurÚcarriers                r)   rè   z)SessionMessagesMixin._insert_message_rowsû  sD  € ô —‘“ˆØ&'Ð'ˆÐ#Øò 	;ˆCØ—7‘7˜6 9Ó-ˆDÜ*¨3¯7©7°<Ó+@ÓAˆJÜ 1°#·'±'¸+Ó2FÈÓ OÐØ—,‘,Ô2°D×4LÑ4LØ˜D # zÐ3DÐUYÐ]hÑUhð 5Mó 5jó kˆCð  1ˆC�ÑØ�}‰}Ð(Ø!$§¡��I‘Ø˜‰MˆHØÔ 1°*Ó =Ñ=ÐÜ˜Ð!2Ó3°dÑ:‰Fð	;ô  -¨XÐ7NÓOˆØ�aŠ<œJ x°Ñ'8×'<Ñ'<¸YÓ'GÌÔMØ×/Ñ/°°jÀ(È7ÑBSÐT]ÑB^Ô_ØÐ)Ð)Ð)r1   c                ó^  — d}|j                  t        ||f«      j                  «       D ]|  \  }}t        |dd«      }t	        |t
        «      rt        d„ |D «       «      sŒ6|D �cg c]  }t        |«      rŒ|‘Œ }	}|j                  t        | j                  |	«      |f«       |dz  }Œ~ |S c c}w )uá  Rewrite older active assistant rows so only the row *carrier_row_id* keeps a ``type: "compaction"``
        checkpoint (durable twin of ``context_compressor.drop_shadowed_checkpoints``). Under native compaction
        every assistant response re-persists a ~120 KB checkpoint the wire builder will never replay once a
        newer one lands, and local compaction â€” the only other prune site â€” rarely fires (#102374).
        Non-checkpoint items stay; returns rows rewritten.r   Nz7Ignoring malformed codex_reasoning_items on message rowc              3  ó2   K  — | ]  }t        |«      –— Œ y ­wr’   )r
   )rQ   Úitems     r)   rS   zFSessionMessagesMixin._drop_shadowed_checkpoint_rows.<locals>.<genexpr>  s   è ø€ Ò5bÐTXÔ6IÈ$×6OÑ5bùs   ‚r   )
rV   Ú_SHADOWED_CHECKPOINT_ROWS_SQLr  r*   r3   r9   Úanyr
   Ú_SET_CODEX_REASONING_SQLrz   )
rY   rZ   r[   Úcarrier_row_idÚ	rewrittenr  r'   Úitemsr/  Úkepts
             r)   r)  z3SessionMessagesMixin._drop_shadowed_checkpoint_rows  s³   € ð ˆ	ØŸ<™<Ô(EÈ
ÐTbÐGcÓd×mÑmÓoò 	‰KˆF�CÜ˜S $Ð(aÓbˆEÜ˜e¤TÔ*´#Ñ5bÐ\aÔ5bÔ2bØØ%*ÖL˜TÔ2EÀdÕ2K’DÐLˆDÐLØ�L‰LÔ1°D×4MÑ4MÈdÓ4SÐU[Ð3\Ô]Ø˜‰N‰Ið	ð Ðùò Ms   Á#B*Á4B*c                óV   ‡ ‡‡‡‡‡‡— ddl mŠ ˆˆˆˆˆˆ ˆfd„}‰ j                  |«       y)u\  Atomically replace a session's messages (/retry, /undo, /compress). DESTRUCTIVE by default (rows
        DELETEd, leave FTS). ``active_only`` spares soft-archived rows (needed with in-place compaction).
        ``archive_dropped`` SOFT-archives live rows rewind-style: what rewind/edit/regenerate must use, since
        DELETE leaves nothing to recover. ``reject_active_turn_lease``: in-txn lease check for user rewrites.

        Pass ``archive_dropped=True`` to SOFT-archive the live rows instead of DELETEing them: the replaced
        turns stay on disk with ``active = 0``, ``compacted = 0`` â€” the same "the user took it back" marking
        :meth:`rewind_to_message` applies â€” and stay readable via :meth:`get_messages` with
        ``include_inactive=True``. This is the mode a rewind/edit/regenerate must use: those flows overwrite
        a transcript the user may not have meant to drop, and a plain DELETE also evicts the rows from the
        FTS index, leaving nothing to recover from (#82756). It implies active-only handling â€”
        already-archived rows are never touched â€” so ``active_only`` is redundant with it. Live rows that
        *messages* keeps in place (the common in-order prefix, matched on role/content/tool identity) are
        left untouched with their ids; only the divergent live suffix is archived and only the new suffix
        of *messages* is inserted (#82956: archiving and re-inserting the kept prefix grew the archive by
        the whole transcript on every rewind). The live view is identical either way; only the durability
        of the dropped turns differs.
        r   )r~   c                ó^  •— ‰
r‰j                  | ‰d dd¬«       n6t        | j                  t        ‰f«      j	                  «       «      r ‰‰«      ‚dx}}‰r‚| j                  t
        ‰t        ‰	«      dz   f«      j                  «       }‰j                  |‰	«      }t        d„ |d | D «       «      }|t        |«      k  r5| j                  d‰||   d   f«       n| j                  d‰rdnd	› �‰f«       ‰j                  | ‰‰	|d  «      \  }}| j                  t        › d
�||z   ||z   ‰f«       y )NT©r‰   rŠ   r   r   c              3  ó<   K  — | ]  }t        |d    d¬«      –— Œ y­w)é   r   ©r=   N©r?   ©rQ   rH   s     r)   rS   zESessionMessagesMixin.replace_messages.<locals>._do.<locals>.<genexpr>F  s   è ø€ Ò%_ÈC¤o°c¸!±fÀQ×&GÐ&GÑ%_ùó   ‚zNUPDATE messages SET active = 0 WHERE session_id = ? AND active = 1 AND id >= ?ú)DELETE FROM messages WHERE session_id = ?ú AND active = 1rO   r½   )r�   r   rV   r   rW   Ú_LIVE_IDENTITY_SQLr:   r  Ú_stamp_kept_live_prefixrë   rè   Ú_SET_COUNTERS_SQL)rZ   r6  Úkept_tool_callsÚliver¾   Úinserted_tool_callsr~   Úactive_onlyÚarchive_droppedrâ   r‰   rY   r[   s         €€€€€€€r)   rË   z2SessionMessagesMixin.replace_messages.<locals>._do:  s^  ø€ Ù'Ø×3Ñ3Ø˜* dÀTÐjnð 4õ pä& t§|¡|´NÀZÀMÓ'R×'[Ñ'[Ó']Ô^Ù3°JÓ?Ð?Ø%&Ð&ˆD�?Ùð —|‘|Ô$6¸ÄSÈÃ]ÐUVÑEVÐ8WÓX×aÑaÓc�Ø×3Ñ3°D¸(ÓC�Ü"%Ñ%_ÐSWÐX]ÐY]ÐS^Ô%_Ó"_�Øœ#˜d›)Ò#à—L‘LÐ!qØ",¨d°4©j¸©mÐ!<õ>ð —‘ÐHÑ^iÑIZÐoqÐHrÐsð  wAð  vCô  DØ,0×,EÑ,EÀdÈJÐX`ÐaeÐafÐXgÓ,hÑ)ˆHÐ)Ø�L‰LÔ-Ð.¨mÐ<Ø ™/¨?Ð=PÑ+PÐR\Ð]õ_r1   N)r€   r~   rÏ   )rY   r[   râ   rH  rI  r‰   rË   r~   s   `````` @r)   Úreplace_messagesz%SessionMessagesMixin.replace_messages%  s%   þ€ õ( 	F÷	_ò 	_ð, 	×Ñ˜CÕ r1   c                ó|   — || j                  | j                  ||«      «      ||rt        j                  |«      fS dfS )uü   The (role, content, tool_call_id, tool_calls) columns a message writes â€” the identity the
        kept-prefix match compares. *content* is passed through the loader's lens first so a message
        read back from the DB matches the row it came from.N)rg   Ú_loaded_view_contentr!   rb   )re   rž   rf   rœ   r6   s        r)   Ú_row_identityz"SessionMessagesMixin._row_identityR  sM   € ð
 �c×)Ñ)¨#×*BÑ*BÀ4ÈÓ*QÓRÐT`Ù*4”—
‘
˜:Ó&ð@ð 	@Ø:>ð@ð 	@r1   c                óÄ  — d}t        ||«      D ]Î  \  }}|j                  dd«      }|d   |k7  s|d   |j                  d«      k7  r |S | j                  ||j                  d«      |j                  d«      t        |j                  d«      «      «      }|| j                  |d   | j	                  |d	   «      |d   t        |d
   «      «      k7  r |S |d   |d<   |dz  }ŒÐ |S )z¹Length of the in-order prefix of *messages* already present as the leading live rows; each
        matched message gets its existing ``_row_id`` stamped, as a fresh insert would set it.r   rž   r'  r   é   rœ   rf   r6   rt   r;  r(  )Úzipr—   rM  r7   rj   )rY   rF  râ   r6  rH   rš   rž   Úidentitys           r)   rC  z,SessionMessagesMixin._stamp_kept_live_prefixZ  s  € ð ˆÜ˜D (Ó+ò 
	‰HˆC�Ø—7‘7˜6 9Ó-ˆDà�1‰v˜Š~  Q¡¨3¯7©7°>Ó+BÒ!BØð ˆð ×)Ñ)¨$°·±¸	Ó0BÀCÇGÁGÈNÓD[Ü*;¸C¿G¹GÀLÓ<QÓ*RóTˆHà˜4×-Ñ-¨c°!©f°d×6JÑ6JÈ3ÈqÉ6Ó6RÐTWÐXYÑTZÔ\mÐnqÐrsÑntÓ\uÓvÒvØð ˆð ! ™VˆC�	‰NØ�A‰I‰Dð
	ð ˆr1   c                ó`   — | dv r)t        |t        «      rt        |«      j                  «       S |S )zÉContent as ``_rows_to_messages`` hands it to callers: user/assistant strings are sanitized and
        stripped on load, so a reloaded session's messages must be compared to rows through the same lens.>   r×   r  )r3   r4   r   rP   )rž   rf   s     r)   rL  z)SessionMessagesMixin._loaded_view_contentk  s/   € ð Ð(Ñ(¬Z¸ÄÔ-EÜ# GÓ,×2Ñ2Ó4Ð4Øˆr1   c                ó,   — | j                  d|f«      duS )u}  True if the session has any soft-archived (``active = 0``) rows (tests/diagnostics).

        Cheap existence probe â€” does not load rows. NOTE: production rewrite paths no longer branch on this
        (they pass ``active_only=True`` unconditionally â€” a probe can fail open or race a concurrent
        ``archive_and_compact``, #80216); kept for tests and diagnostics.
        zBSELECT 1 FROM messages WHERE session_id = ? AND active = 0 LIMIT 1Nr!  ©rY   r[   s     r)   Úhas_archived_messagesz*SessionMessagesMixin.has_archived_messagess  s)   € ð �~‰~ØPÐS]ÐR_óaØhlðmð 	mr1   c                óF   — |syt        | j                  d|f«      d   «      S )z£MAX(id) of the active rows (0 if none), captured at compression START: every active row above it
        arrived concurrently and must survive compaction verbatim.r   zMSELECT COALESCE(MAX(id), 0) FROM messages WHERE session_id = ? AND active = 1)r`   r  rT  s     r)   Úget_active_message_watermarkz1SessionMessagesMixin.get_active_message_watermark}  s7   € ñ ØÜ�4—>‘>Ø[Ð^hÐ]jólØlmñoó pð 	pr1   c                ó¨   — |j                  ||«      j                  «       }|D �cg c]  }t        |d   «      ‘Œ c}t        d„ |D «       «      fS c c}w )ze``(ids, tool_call_count)`` of the concurrent-tail rows selected by *sql* (``SELECT id, tool_calls``).r   c              3  ó8   K  — | ]  }t        |d    «      –— Œ y­w)r6   Nr=  )rQ   ró   s     r)   rS   zBSessionMessagesMixin._tail_rows_after_watermark.<locals>.<genexpr>ˆ  s   è ø€ Ò0`ÐVW´ÀÀ<Á×1QÑ0`ùs   ‚)rV   r  r`   rë   )rY   rZ   r  rÁ   Úrowsró   s         r)   Ú_tail_rows_after_watermarkz/SessionMessagesMixin._tail_rows_after_watermark…  sH   € à�|‰|˜C Ó(×1Ñ1Ó3ˆØ&*Ö+ ”�A�d‘G•Ò+¬SÑ0`Ð[_Ô0`Ó-`Ð`Ð`ùÒ+s   ¥A©r[   c               óð   ‡— |du}d|rdndz   Šdj                  ˆfd„| j                  |«      D «       «      }|j                  d|› d|rdnd	› d
|› d|rdnd	› dt        |«      › d�|r
|g|¢«       y|«       y)zÏPure-SQL clone of *tail_ids* as fresh live rows (new id/display order, active=1, compacted=0;
        message payload columns stay byte-exact and FTS triggers index the clones), into *session_id* when given.N)r   Úactiver   r   r\  r»   z, c              3  ó,   •K  — | ]  }|‰vsŒ|–— Œ y ­wr’   r»   )rQ   ÚcÚskips     €r)   rS   z;SessionMessagesMixin._clone_message_rows.<locals>.<genexpr>’  s   øè ø€ ÒZ 1ÈAÐUYÊMœQÑZùs   ƒ	�zINSERT INTO messages (úsession_id, rO   zactive, compacted) SELECT z?, z 1, 0 FROM messages WHERE id IN (z) ORDER BY id)ÚjoinÚ_message_column_namesrV   r   )rY   rZ   Útail_idsr[   ÚretargetÚcol_listra  s         @r)   Ú_clone_message_rowsz(SessionMessagesMixin._clone_message_rowsŠ  s¦   ø€ ð  TÐ)ˆð >ÑT\ÁÐbdÑeˆØ—9‘9ÓZ¨×(BÑ(BÀ4Ó(HÔZÓZˆØ�‰Ø$ X J¨bÁ8±ÐQSÐ0Tð UØ�Z˜r©8¡%¸Ð!<ð =Ü)¨(Ó3Ð4°MðCñ (0ˆZÐ#˜(Ð#õ		?ð 6>õ		?r1   c                óX   ‡ ‡‡‡‡‡‡‡— ddl mŠ ˆˆˆˆˆ ˆˆˆfd„}‰ j                  |«      S )u’  Non-destructive in-place compaction under ONE session id: soft-archive the active rows (``active=0,
        compacted=1``: summarized away, still searchable) and insert *compacted_messages* as fresh active
        rows, atomically; returns the new ACTIVE count (= ``message_count``). *watermark* (compression
        START): rows ``id > watermark`` arrived during the slow summary and are re-sequenced after the
        compacted set by a pure-SQL clone (fresh ids); ``None`` archives everything. *lock_holder*: verified
        in-txn so a reclaimed lease fails instead of clobbering the winner. *tail_count*: the LAST N compacted
        rows are the verbatim carried tail; their originals and the clones' originals are superseded
        duplicates and get rewind flags (``active=0, compacted=0``) so search doesn't return each carried
        message once per compaction. ``model_config_patch`` merges in the same txn (``None`` removes a key).

        Concurrent-append safety (#75316): when *watermark* is provided (the value of
        :meth:`get_active_message_watermark` captured at compression START), rows that arrived during the
        slow provider summary call (``id > watermark``) are NOT summarized away. They are re-sequenced after
        the compacted set by a pure-SQL column clone (every column except ``id`` â€” content, api_content,
        platform_message_id, token counts, reasoning sidecars all survive byte-exact, and the FTS triggers
        index the clones naturally), and the originals are archived. NOTE: re-sequencing assigns the tail
        rows fresh ids; consumers that reference durable row ids re-resolve by content (see 3e8ab0610).
        r   r|   c                óÀ  •— ‰�^| j                  t        ‰f«      j                  «       }|�+|d   ‰k7  s#t        |d   «      t	        j                  «       k  r ‰d‰›d�«      ‚‰d u}|r‰j                  | ‰‰d¬«      nd }‰€g dfn‰j                  | d‰t        ‰«      f«      \  }}g }‰dkD  rb‰d u}| j                  d	|rd
nd› d�‰g|rt        ‰«      fnd¢t        ‰«      ‘­«      j                  «       D �cg c]  }t        |d   «      ‘Œ }}||z  }|rDt        |«      }	| j                  d|	› d�‰g|¢«       | j                  t        › d|	› d�‰g|¢«       n| j                  t        ‰f«       ‰j                  | ‰‰«      \  }
}|r%‰j                  | |«       |
t        |«      z  }
||z  }| j                  t        › |rdnd› d�|
|g|r|fnd¢‰‘­«       |
S c c}w )NrF   rE   zCompression lease for z; lost before commit; refusing to publish a stale compactionÚraise)Ú
on_missingr   z^SELECT id, tool_calls FROM messages WHERE session_id = ? AND active = 1 AND id > ? ORDER BY idz;SELECT id FROM messages WHERE session_id = ? AND active = 1z AND id <= ?rO   z ORDER BY id DESC LIMIT ?r»   r   zNUPDATE messages SET active = 0, compacted = 0 WHERE session_id = ? AND id IN (r   z AND id NOT IN (z, model_config = ?r½   )rV   r   rW   rG   r�   Ú_merge_model_config_jsonr[  r`   r  r   Ú_ARCHIVE_ACTIVE_SQLrè   rh  r:   rD  )rZ   Úlock_rowÚpatchÚpatched_model_configre  Útail_tool_callsÚ
rewind_idsÚboundrH   Úplaceholdersr¾   rê   r}   Úcompacted_messagesÚlock_holderÚmodel_config_patchrY   r[   Ú
tail_countÚ	watermarks               €€€€€€€€r)   rË   z5SessionMessagesMixin.archive_and_compact.<locals>._do¯  sz  ø€ ØÐ&ØŸ<™<Ô(AÀJÀ=ÓQ×ZÑZÓ\�ØÐ# x°Ñ'9¸[Ò'HÌEÐRZÐ[gÑRhÓLiÔmq×mvÑmvÓmxÒLxÙ;Ø0°°Ð>yÐzó|ð |à&¨dÐ2ˆEñ NSð $(×#@Ñ#@Ø�jÐ"4Àð $Aô $JØX\ð !à3<Ð3D¨¨Q©È$×JiÑJiØÐvØœS ›^Ð,óK.Ñ%ˆH�oð
 %'ˆJØ˜AŠ~Ø!¨Ð-�Ø8<¿¹ØQÑdiÑR`ÐoqÐQrð s/ð /àÐX¹¤C¨	£NÑ#4À2ÐXÌÈJËÑXó9Z÷ [cÑZbÓZdöf°œc # d¡)�nð f�
ð fð ˜(Ñ"ˆJÙÜ,¨ZÓ8�Ø—‘ð 7Ø7C°nÀAðGØISÐHaÐV`ÐHaôcà—‘Ô 3Ð4Ð4DÀ\ÀNÐRSÐTÐWaÐVoÐdnÐVoÕpà—‘Ô0°:°-Ô@Ø)-×)BÑ)BÀ4ÈÐUgÓ)hÑ&ˆHÐ&ÙØ×(Ñ(¨¨xÔ8ØœC ›MÑ)�Ø  OÑ3Ð Ø�L‰LÔ-Ð.ÁuÑ/CÐRTÐ.UÐUbÐcØÐ+ÐeÉ%Ð0DÑ/FÐUWÐeÐZdÑeôgàˆOùò'fs   Ã8G)r   r}   rÏ   )	rY   r[   rv  rx  rz  rw  ry  rË   r}   s	   ``````` @r)   Úarchive_and_compactz(SessionMessagesMixin.archive_and_compact™  s'   ÿ€ õ* 	C÷%	ó %	ðL ×"Ñ" 3Ó'Ð'r1   c                ó¨   — t        | dd«      s5|j                  d«      j                  «       D �cg c]  }|d   ‘Œ	 c}| _        | j                  S c c}w )z>Column names of the messages table, cached per-connection era.Ú_message_columns_cacheNzPRAGMA table_info(messages)r   )ÚgetattrrV   r  r}  )rY   rZ   ró   s      r)   rd  z*SessionMessagesMixin._message_column_names×  sK   € ä�tÐ5°tÔ<Ø9=¿¹ÐFcÓ9d×9mÑ9mÓ9oÖ*p°A¨1¨Q«4Ò*pˆDÔ'Ø×*Ñ*Ð*ùò +qs   ¯Ac                ó\   — | j                  dt        |«      || j                  |«      f«      S )ub  Backfill the ``api_content`` sidecar onto the newest ACTIVE user row (0/1 rows). Preflight compaction
        inserts that row BEFORE the sidecar exists and the later persist identity-skips compacted dicts;
        without this a reload reopens the prompt-cache divergence. ``content`` match guards a racing rewrite.

        POSITIONAL, and only safe when the caller already knows the newest
        active user row IS the message it stamped. The content match is NOT
        sufficient on its own: repeated identical user turns ("ok", "y",
        "continue") make an OLDER row compare equal, so calling this before
        the current turn's row exists overwrites the previous turn's sidecar
        with this turn's bytes â€” durable wrong-bytes replay, a worse cache
        break than the missing sidecar. When the caller holds the durable row
        id (``_row_id``, synced onto the live dict by
        ``sync_flushed_message_markers`` and stamped by
        :meth:`_insert_message_rows`), use :meth:`set_message_api_content`
        instead â€” it addresses the exact row and cannot land on a neighbour.
        z¨UPDATE messages SET api_content = ? WHERE id = (SELECT id FROM messages WHERE session_id = ? AND role = 'user' AND active = 1 ORDER BY id DESC LIMIT 1) AND content IS ?)Ú_write_rowcountrB   rg   )rY   r[   rf   r¯   s       r)   Úset_latest_user_api_contentz0SessionMessagesMixin.set_latest_user_api_contentÝ  s:   € ð" ×#Ñ#ð!ô ˜{Ó+¨Z¸×9MÑ9MÈgÓ9VÐWó	Yð 	Yr1   c           	     ó®   — |r%t        |t        «      st        |t        «      r|dk  ry| j                  dt	        |«      ||| j                  |«      f«      S )ae  Backfill the ``api_content`` sidecar onto ONE known durable row.

        Row-addressed counterpart to :meth:`set_latest_user_api_content`: the
        caller passes the ``_row_id`` the write path stamped on the live
        message dict, so the update cannot drift onto a neighbouring row that
        merely carries the same text.

        Used by the turn prologue whenever the current turn's user row was
        already materialized before the sidecar could be composed (in-place
        preflight compaction, a close/early flush that raced the prologue).
        The crash persist then marker-skips that message, so this is the only
        way the stamped bytes reach the store.

        ``active = 1`` and the ``content`` match stay as defensive guards: a
        row the compaction archived, or one a racing rewrite changed, is left
        untouched.
        r   zuUPDATE messages SET api_content = ? WHERE id = ? AND session_id = ? AND role = 'user' AND active = 1 AND content IS ?)r3   Úboolr`   r€  rB   rg   )rY   r[   r  rf   r¯   s        r)   Úset_message_api_contentz,SessionMessagesMixin.set_message_api_contentô  s^   € ñ( œZ¨´Ô5¼ZÈÔPSÔ=TÐX^ÐbcÒXcØØ×#Ñ#ð@ä˜{Ó+¨V°ZÀ×AUÑAUÐV]ÓA^Ð_óað 	ar1   c                óš   — |r%t        |t        «      st        |t        «      r|dk  ry| j                  d| j	                  |«      ||f«      S )a‚  Rewrite the content of ONE known active user row. Used when a user turn was written at submit
        time (before the agent ran) and the turn prologue then rewrote the prompt it persists (@-file
        expansion, native image parts): the early row must show what the transcript will replay, not the
        raw keystrokes, and the turn must not append a second row for the same input.r   z`UPDATE messages SET content = ? WHERE id = ? AND session_id = ? AND role = 'user' AND active = 1)r3   rƒ  r`   r€  rg   )rY   r[   r  rf   s       r)   Úset_user_message_contentz-SessionMessagesMixin.set_user_message_content  sS   € ñ
 œZ¨´Ô5¼ZÈÔPSÔ=TÐX^ÐbcÒXcØØ×#Ñ#ØnØ×!Ñ! 'Ó*¨F°JÐ?óAð 	Ar1   c           	     ó  — |d   }|d   dk(  r^t        d| j                  |d   «      |d   | j                  |d   «      dœ«      \  }}|�"|� | j                  |j	                  d«      «      }|d   ||d   |d   |d	   |d
   fS )zZHistorical display identity, including normalized live content from user handoff carriers.rf   rž   r×   r�   rq   )rž   rf   r�   rq   rŸ   rœ   r6   r›   )r   rj   rx   rg   r—   )rY   rH   Údedupe_contentÚhandoffÚ	live_views        r)   r±   z(SessionMessagesMixin._display_dedupe_key  sµ   € à˜Y™ˆØˆv‰;˜&Ò Ü!;Ø¨4×+?Ñ+?ÀÀIÁÓ+OØ # NÑ 3Ø$(×$AÑ$AÀ#ÐFXÑBYÓ$Zñ=\ó "]ÑˆG�Yð Ð" yÐ'<Ø!%×!5Ñ!5°i·m±mÀIÓ6NÓ!O�Ø�F‘˜^¨S°Ñ-=Ø�NÑ# S¨Ñ%6¸¸KÑ8HðJð 	Jr1   c                óz   — t        j                  t        | «      j                  dd«      «      j	                  «       S )zCFixed-width durable identity for indexed display-generation lookup.zutf-8Úsurrogatepass)ÚhashlibÚsha256ÚreprÚencodeÚdigest©r˜   s    r)   r°   z&SessionMessagesMixin._display_identity'  s-   € ô �~‰~œd 3›i×.Ñ.¨w¸ÓHÓI×PÑPÓRÐRr1   c                ó8  — i }i }|D ]e  }| j                  |«      }|j                  |«      }|�|d   |d   f|d   |d   fkD  r|||<   t        |j                  ||d   «      |d   «      ||<   Œg t        ||j                  ¬«      D �cg c]  }||   ‘Œ	 c}S c c}w )a;  Collapse compaction generations so each logical message appears once (the protected tail is copied
        into each generation: same role/content/timestamp, different ``active``/id); prefer the live row, then
        the newest. The ONE definition every display projection shares. *rows* must be ordered by ``id``.r^  r   r’  )r±   r—   ÚminÚsortedÚ__getitem__)rY   rZ  r  Úfirst_idrH   r˜   r+  s          r)   Ú_dedupe_display_generationsz0SessionMessagesMixin._dedupe_display_generations,  sµ   € ð ,.ˆØ/1ˆØò 	IˆCØ×*Ñ*¨3Ó/ˆCØ—(‘(˜3“-ˆCØˆ{˜s 8™}¨c°$©iÐ8¸CÀ¹MÈ3ÈtÉ9Ð;UÒUØ��S‘	Ü §¡¨S°#°d±)Ó <¸cÀ$¹iÓHˆH�SŠMð	Iô &,¨D°h×6JÑ6JÔ%KÖL˜c��S“	ÒLÐLùÒLs   ÂBc                ó   ‡ ‡‡— ‰ j                  «       5 }t        ‰ j                  |«      «      }ddd«       ddhk  sydŠ‰ j                  ‰‰f«      €yt	        ‰ dd«      ryˆˆ ˆfd„}t        ‰ j                  |«      «      S # 1 sw Y   ŒVxY w)	zTBackfill one legacy session once, preserving the pre-index display identity exactly.Nr   Údisplay_identityFz�SELECT 1 FROM messages WHERE session_id = ? AND (active = 1 OR compacted = 1) AND (display_order IS NULL OR display_identity IS NULL) LIMIT 1TÚ	read_onlyc                ó   •— | j                  ‰
‰f«      j                  «       }|€yi }d}	 | j                  d‰|f«      }|}g }|D ]^  }|d   }‰j                  ‰j                  |«      «      }|j	                  ||«      }	|	|d   k7  s	||d   k7  sŒK|j                  |	||f«       Œ` |j                  «        ||k(  r	 y| j                  d|«       Œ¥)NTr   a  SELECT id, role, content, timestamp, tool_call_id, tool_calls, tool_name, display_kind, display_metadata, display_order, display_identity FROM messages INDEXED BY idx_messages_session_id WHERE session_id = ? AND id > ? AND (active = 1 OR compacted = 1) ORDER BY id LIMIT 1000r   r   rš  zHUPDATE messages SET display_order = ?, display_identity = ? WHERE id = ?)rV   rW   r°   r±   Ú
setdefaultrü   ÚcloseÚexecutemany)rZ   Úmissingr—  Úlast_idrZ  Úbatch_startÚupdatesrH   rQ  ÚorderÚmissing_sqlrY   r[   s             €€€r)   rË   z7SessionMessagesMixin._ensure_display_order.<locals>._doJ  s  ø€ Ø—l‘l ;°°Ó>×GÑGÓIˆGØˆØØ)+ˆHØˆGØØ—|‘|ð-ð
   Ð)ó+�ð &�Ø�Øò C�CØ! $™i�GØ#×5Ñ5°d×6NÑ6NÈsÓ6SÓT�HØ$×/Ñ/°¸'ÓB�EØ  OÑ 4Ò4¸ÀCÐHZÑD[Ó8[ØŸ™¨¨x¸Ð'AÕBðCð —
‘
”Ø˜kÒ)Øð ð × Ñ Ø^Ð`gôið' r1   )Ú	_read_ctxÚsetrd  r  r~  rƒ  rÏ   )rY   r[   rZ   ÚcolumnsrË   r¥  s   ``   @r)   Ú_ensure_display_orderz*SessionMessagesMixin._ensure_display_order<  sš   ú€ à�^‰^Óð 	< Ü˜$×4Ñ4°TÓ:Ó;ˆG÷	<àÐ!3Ð4¸Ò?ØðNð 	ð �>‰>˜+¨
 }Ó5Ð=ØÜ�4˜ eÔ,Øö	ô: �D×'Ñ'¨Ó,Ó-Ð-÷S	<ð 	<ús   ”BÂBc               óÚ  — i }| j                  «       5 }|j                  d«       	 |j                  dd«      j                  «       du}|rdnd}	|j                  d|	› d|› d	�|f«      }
|
D ]J  }| j                  | j	                  |«      «      }|j                  |«      }|d
   |d   f}|�||kD  sŒF|||<   ŒL |
j                  «        t        |«      }|r|ddd…   |d d| ddd…   n||d d| }|D �cg c]
  }||   d   ‘Œ }}i }t        dt        |«      d«      D ]Q  }|||dz    }|j                  |j                  d|› dt        |«      › d�|g|¢­«      D �ci c]  }|d   |“Œ
 c}«       ŒS |D �cg c]  }||v sŒ||   ‘Œ c}|j                  r|j                  d«       cddd«       S c c}w c c}w c c}w # |j                  r|j                  d«       w w xY w# 1 sw Y   yxY w)zNProject a legacy read-only display page without retaining transcript payloads.ÚBEGINz=SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ?)Úidx_messages_session_idNz"INDEXED BY idx_messages_session_idzNOT INDEXEDzSELECT id, role, content, timestamp, tool_call_id, tool_calls, tool_name, active, display_kind, display_metadata FROM messages ú WHERE session_id = ?z ORDER BY id ASCr^  r   éÿÿÿÿr   r   é„  ú+SELECT * FROM messages WHERE session_id = ?z AND id IN (r   ÚROLLBACK)r¦  rV   rW   r°   r±   r—   rž  r9   ru   r:   Úupdater   Úin_transaction)rY   r[   Úactive_clauseÚlimitr  ÚlatestÚrepresentativesrZ   Úhas_session_indexÚ
index_hintrZ  rH   rQ  ÚcurrentÚ	candidateÚ
identitiesÚselected_idsÚselectedrà   Úchunkr  s                        r)   Ú_legacy_display_pagez)SessionMessagesMixin._legacy_display_pagei  sg  € ð 9;ˆØ�^‰^Óð "	- Ø�L‰L˜Ô!ð -Ø$(§L¡LØSØ0ó%÷ ‘(“* Dð%)Ð!ñ FWÑAÐ\i�
Ø—|‘|ðDØDNÀ<ð P+Ø+8¨/Ð9IðKð  �Mó	#�ð
  ò >�CØ#×5Ñ5°d×6NÑ6NÈsÓ6SÓT�HØ-×1Ñ1°(Ó;�GØ!$ X¡°°D±	Ð :�IØ�¨)°gÓ*=Ø4=˜¨Ò1ð>ð —
‘
”ä! /Ó2�
ÙHN˜Z©¨"¨Ñ-¨f¨gÐ6°v¸Ð>¹tÀ¸tÒDÐT^Ð_eÐ_fÐTgÐhnÐinÐTo�
ØMWÖXÀ °Ñ 9¸!Ó <ÐX�ÐXØ�Ü" 1¤c¨,Ó&7¸Ó=ò 0�EØ(¨¨u°s©{Ð;�EØ—O‘O¸t¿|¹|ØEÀmÀ_ð U&Ü&3°EÓ&:Ð%;¸1ð>à#Ð, eÑ,ó@.ö %/¸ S¨¡Y°¡^ò %/õ 0ð0ð 8DÖZ¨VÀvÐQYÒGY˜ Ó(ÒZà×&Ò&Ø—L‘L Ô,÷E"	-ñ "	-ùò.  Yùò%/ùò [øà×&Ò&Ø—L‘L Õ,ð 'ú÷C"	-ð "	-ús[   “G!¦B	F>Â0AF>Ã4F/ÄAF>ÅF4Å(F>Å5	F9Å?F9ÆF>ÆG!Æ/F>Æ> GÇG!Ç!G*c               ó  — t        |«      }|j                  dd«       |j                  dd«       |r|j                  dd«      rd|d<   | j                  |d   «      |d<   |j                  d«      rt	        |d   g d	|› d
�«      |d<   |j                  d«      �| j                  |d   «      |d<   t        |j                  «       «      D ]6  \  }}|t        vsŒt        |t        t        f«      sŒ&|j                  |«       Œ8 |S )z†``dict(row)`` with content/tool_calls/display_metadata decoded; *summary_flag* keeps
        ``_compressed_summary`` only as ``True``.rš  Nr   r®   r   Trf   r6   z$Failed to deserialize tool_calls in z, falling back to []rq   )rm   rý   rj   r—   r*   rx   r9   r5  Ú_MESSAGE_SCHEMA_KEYSr3   r_   Ú	bytearray)rY   rH   Úwarn_contextÚsummary_flagrš   r˜   r-   s          r)   Ú_row_to_message_dictz)SessionMessagesMixin._row_to_message_dict‘  s  € ô �3‹iˆØ�‰Ð" DÔ)Ø�‰� Ô&Ù˜CŸG™GÐ$9¸1Ô=Ø)-ˆCÐ%Ñ&Ø×-Ñ-¨c°)©nÓ=ˆˆI‰Ø�7‰7�<Ô Ü (Ø�LÑ! 2Ð)MÈlÈ^Ð[oÐ'pó!rˆC�Ñà�7‰7Ð%Ó&Ð2Ø&*×&CÑ&CÀCÐHZÑD[Ó&\ˆCÐ"Ñ#ô ˜sŸy™y›{Ó+ò 	‰JˆC�ØÔ.Ò.´:¸eÄeÌYÐEWÕ3XØ—‘˜•ð	ð ˆ
r1   c                ó   — | rdS |rt         S dS )zcAudit: every row; display: active plus compaction-archived (never Undo/Rewind rows); default: live.rO   rA  )r  )Úinclude_inactiveÚinclude_compacteds     r)   Ú_active_clausez#SessionMessagesMixin._active_clause©  s   € ñ &ˆrÐoÑFWÔ,BÐoÐ]nÐor1   c                ó  — |�|s|rt        d«      ‚|�|rt        d«      ‚| j                  ||«      }|r:|s8| j                  |«      r'|rdnd}	d|	› d�}
| j                  |
||€dn|||g«      }nx|r| j	                  |||||¬«      }n_d	|› |�d
nd› d|rdnd› �}
|€|gn||g}|€|r|
dz  }
|j                  |€dn||g«       | j                  |
|«      }|r|j                  «        |D �cg c]  }| j                  |dd¬«      ‘Œ c}S c c}w )a0  Load messages in insertion order (id, never timestamp: clocks regress). ``include_inactive``:
        rewind rows too; ``include_compacted``: compaction-archived display history (not rewind rows).
        ``latest`` pages back from the newest but returns chronological order; ``after_id``: keyset paging.z2after_id is incompatible with latest/offset pagingzYafter_id is incompatible with include_compacted (deduped display reads use offset paging)ÚDESCÚASCzÓWITH page AS (
                    SELECT display_order FROM messages
                    WHERE session_id = ? AND (active = 1 OR compacted = 1)
                    GROUP BY display_order ORDER BY display_order a3  
                    LIMIT ? OFFSET ?
                )
                SELECT chosen.* FROM page
                JOIN messages AS chosen ON chosen.id = (
                    SELECT candidate.id FROM messages AS candidate
                    WHERE candidate.session_id = ?
                      AND candidate.display_order = page.display_order
                      AND (candidate.active = 1 OR candidate.compacted = 1)
                    ORDER BY candidate.active DESC, candidate.id DESC LIMIT 1
                )
                ORDER BY page.display_order ASCr®  )r´  rµ  r  r¶  r°  z AND id > ?rO   z ORDER BY id z LIMIT ? OFFSET ?Úget_messagesT©rÄ  rÅ  )rc   rÊ  r©  Ú	_read_allrÀ  ÚextendÚreverserÆ  )rY   r[   rÈ  rÉ  rµ  r  r¶  Úafter_idr´  Ú	directionr  rZ  rÁ   rH   s                 r)   rÎ  z!SessionMessagesMixin.get_messages®  ss  € ð Ð¡V©vÜÐQÓRÐRØÐÑ$5ÜÐxÓyÐyØ×+Ñ+Ð,<Ð>OÓPˆÙÑ%5¸$×:TÑ:TÐU_Ô:`Ù"(™¨eˆIðCð DMÀ+ð N0ð3ˆCð —>‘> #¨
¸%¸-±BÈUÐTZÐ\fÐ'gÓh‰DÙð ×,Ñ,Ø¨-¸uÈVÐ\bð -ó d‰Dð AÀÀØ$,Ð$8‘=¸bÐAÀÑY_ÉvÐejÐNkðmˆCà+3Ð+;˜J™<À*ÈhÐAWˆFØÐ ¡FàÐ*Ñ*�Ø—‘ U ]™r¸¸vÐFÔGØ—>‘> # vÓ.ˆDÙØ—‘”ØjnÖoÐcf�×)Ñ)¨#¸NÐY]Ð)Õ^ÒoÐoùÒos   Ã+D	c           
     óü   ‡— |D �cg c]  }|sŒ|‘Œ	 c}Šˆfd„t        dt        ‰«      d«      D «       }|D ��cg c]1  }| j                  dt        |«      › d�|«      D ]  }|d   |d   dœ‘Œ Œ3 c}}S c c}w c c}}w )ziTool results containing ``/pull/``: a deliberately loose scan, oldest-first so the caller takes the last.c              3  ó.   •K  — | ]  }‰||d z    –— Œ y­w)r¯  Nr»   )rQ   rà   r  s     €r)   rS   z<SessionMessagesMixin.find_pr_url_messages.<locals>.<genexpr>à  s   øè ø€ ÒP¨u�#�e˜e c™kÔ*ÑPùs   ƒr   r¯  zRSELECT session_id, content FROM messages
                    WHERE session_id IN (zi)
                      AND role = 'tool' AND content LIKE '%/pull/%'
                    ORDER BY id ASCr   )r[   rf   )ru   r:   rÐ  r   )rY   Úsession_idsÚsÚchunksr¿  rH   r  s         @r)   Úfind_pr_url_messagesz)SessionMessagesMixin.find_pr_url_messagesÝ  s™   ø€ à%Ö+�QªŠqÒ+ˆÛP¼¸aÄÀSÃÈ3Ó8OÔPˆØGM÷ ¸eÐY]×YgÑYgð*Ü*7¸Ó*>Ð)?ð @$ð'ð óZò ÐRU˜s 1™v°#°a±&Ó9ð Ð9ó ð 	ùò ,ùós   †A3ŽA3¹6A8c                ó
  — t        |d«      }| j                  «       5 }|j                  d||f«      j                  «       sg dddœcddd«       S |j                  d|||dz   f«      j	                  «       }|j                  d|||f«      j	                  «       }ddd«       g t        «      ¢¢­D �cg c]  }| j                  |dd	¬
«      ‘Œ }}|t        dt        |«      dz
  «      t        |«      dœS # 1 sw Y   Œ]xY wc c}w )zÎUp to *window* messages either side of an anchor id (ascending). ``messages_before``/``_after`` count
        strictly around the anchor (fewer than *window* = session boundary). Empty for a foreign anchor.r   z>SELECT 1 FROM messages WHERE id = ? AND session_id = ? LIMIT 1)ÚwindowÚmessages_beforeÚmessages_afterNzPSELECT * FROM messages WHERE session_id = ? AND id <= ? ORDER BY id DESC LIMIT ?r   zNSELECT * FROM messages WHERE session_id = ? AND id > ? ORDER BY id ASC LIMIT ?Úget_messages_aroundFrÏ  )r…   r¦  rV   rW   r  ÚreversedrÆ  r:   )	rY   r[   Úaround_message_idrÜ  rZ   Úbefore_rowsÚ
after_rowsró   Úwindow_msgss	            r)   rß  z(SessionMessagesMixin.get_messages_aroundè  s6  € ô �V˜Q“ˆØ�^‰^Óð 		D Ø—<‘<Ð `Ø!2°JÐ ?óAßAIÁÃðLà"$¸ÈaÑP÷		Dñ 		Dð Ÿ,™,ØbØÐ.°¸±
Ð;ó=ç=E¹X»Zð ð Ÿ™Ø`ØÐ.°Ð7ó9ç9A¹»ð ÷		Dð !F¤(¨;Ó"7Ð E¸*Ñ EöGØð ×0Ñ0°ÐAVÐejÐ0Õkð Gˆð Gð &¼#¸aÄÀ[ÓAQÐTUÑAUÓ:VÔjmÐnxÓjyÑzÐz÷		Dð 		DüòGs   �(C4ÁA	C4Â2D Ã4C=c                óZ  — |s|S 	 | j                  |«      xs |}| j                  «       5 }|}|h}d}t        d«      D ]¦  }	 |j	                  d|f«      j                  «       �|}|j	                  dt        dd«      › dt        dd«      › dt        dd	«      › d
t        dt        «      › d�	|f«      j                  «       }|�|d   r|d   |v r n|d   }|j                  |«       Œ¨ |�|n|cddd«       S # t        $ r Y Œæw xY w# t        $ r |cY c cddd«       S w xY w# 1 sw Y   yxY w)u   Redirect a resume target to the descendant holding the messages: follow the compression chain to
        the live tip (lineage-aware, so delegate/branch children never hijack it), then walk
        ``parent_session_id`` forward to the DEEPEST node with messages (a continuation may hold newer
        turns), skipping branch/delegate/reset/tool children. Unchanged when nothing has messages; depth cap 32.

        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.
        Né    z3SELECT 1 FROM messages WHERE session_id = ? LIMIT 1zISELECT id FROM sessions AS child WHERE child.parent_session_id = ?   AND zchild.model_configz$._branched_fromz IS NULL   AND z$._delegate_fromz$._reset_fromz IS NULL   AND NOT Úchildza   AND COALESCE(child.source, '') != 'tool' ORDER BY child.started_at DESC, child.id DESC LIMIT 1r   )
Úget_compression_tipÚ	Exceptionr¦  ru   rV   rW   r   r   r   Úadd)rY   r[   rZ   rº  r  Úbestrw   Ú	child_rows           r)   Úresolve_resume_session_idz.SessionMessagesMixin.resolve_resume_session_idû  s•  € ñ ØÐð	Ø×1Ñ1°*Ó=ÒKÀˆJð �^‰^Óð 	< Ø ˆGØ�9ˆDØˆDÜ˜2“Yò "�ð&Ø—|‘|Ð$YÐ\cÐ[eÓf×oÑoÓqÐ}Ø&˜Ø $§¡ð!Ü!2Ð3GÐI[Ó!\Ð ]ð ^!Ü!2Ð3GÐI[Ó!\Ð ]ð ^!Ü!2Ð3GÈÓ!YÐ Zð [%Ü%<¸WÔF\Ó%]Ð$^ð _Pð	Pð SZÐQ[ó!]÷ ^fÑ]eÓ]gð ð Ð$¨I°dªO¸yÈ¹ÐRVÑ?VÙØ# D™/�Ø—‘˜Õ!ð#"ð$  Ð+‘4°÷-	<ñ 	<øô ò 	Ùð	ûô$ !ò &Ø%Ó%÷#	<ñ 	<ð &ú÷!	<ð 	<úsA   †C6 «D!ÁA=DÂ?-D!Ã6	DÄDÄDÄD!ÄDÄD!Ä!D*c          
     ó~   — | j                  d|rdnd› | j                  › dt        |«      › d|› d�t        |«      «      S )zŒ``_CONVERSATION_ROW_COLUMNS`` rows for *session_ids* ORDER BY id (timestamps are not monotonic
        and would break tool-call adjacency).zSELECT rb  rO   z$ FROM messages WHERE session_id IN (r   z ORDER BY id)rÐ  Ú_CONVERSATION_ROW_COLUMNSr   r  )rY   r×  r´  Úwith_session_ids       r)   Ú_fetch_conversation_rowsz-SessionMessagesMixin._fetch_conversation_rows$  sU   € ð �~‰~Ø©‘n¸RÐ@À×A_ÑA_Ð@`ð a2Ü2?ÀÓ2LÐ1MÈQØˆo˜\ð+ä,1°+Ó,>ó@ð 	@r1   c                óÆ   — | j                  |r| j                  |«      n|g| j                  ||«      d¬«      }|r| j                  |«      }| j	                  ||||||¬«      S )aø  Load messages in OpenAI format. ``include_compacted`` (deduped display history) is for DISPLAY reads
        only: the model-fed restore must not regrow what compaction summarized away. ``repair_alternation``
        repairs the loaded list for LIVE REPLAY callers (a durable ``user;user`` pair would re-trigger the
        per-request repair forever), preserving summary markers before repair so derivative context
        cannot merge with an original user turn; the stored transcript is never mutated.F©rð  ©r[   Úinclude_ancestorsÚrepair_alternationÚinclude_row_idsÚinclude_summary_markers)rñ  r	  rÊ  r˜  Ú_rows_to_conversation)rY   r[   rõ  rÈ  rö  r÷  rÉ  rZ  s           r)   Úget_messages_as_conversationz1SessionMessagesMixin.get_messages_as_conversation,  s   € ð ×,Ñ,Ù4EˆD×$Ñ$ ZÔ0ÈJÈ<Ø×ÑÐ 0Ð2CÓDÐV[ð -ó ]ˆñ Ø×3Ñ3°DÓ9ˆDØ×)Ñ)¨$¸:ÐYjØ1À?Ø$6ð *ó 8ð 	8r1   c                óV  ‡
— | j                  |«      d   }| j                  |j                  d«      |«      }|�|j                  |«      ndŠ
d}‰
�$t        ˆ
fd„t	        |«      D «       d«      }|�|df}|€| j                  ||«      }|€d|fS |\  }}	|	r|j                  |«       |	 |fS )aa  Ancestor-lineage dedupe of one decoded user *msg* -> ``(skip, exact_clone_key)``. Rotation
        column-clones the concurrent tail into the child, so copies need not be adjacent: the exact
        ``(timestamp, canonical content)`` clone index is checked first, then the adjacent heuristic. A
        rotated child carrier wins over the ancestor copy.r   rŸ   Nc              3  ó2   •K  — | ]  \  }}|‰u sŒ|–— Œ y ­wr’   r»   )rQ   Úir»  Úprevious_exacts      €r)   rS   z=SessionMessagesMixin._dedupe_replayed_user.<locals>.<genexpr>H  s   øè ø€ Ò"l©¨¨IÐPYÐ]kÒPk¤1Ñ"lùs   ƒ�TF)Ú _canonical_replayed_user_contentÚ_exact_replayed_user_clone_keyr—   rû   Ú	enumerateÚ%_find_duplicate_replayed_user_messagerý   )rY   râ   rš   Úexact_user_clonesÚcanonical_contentÚexact_clone_keyÚ	duplicateÚprevious_indexÚduplicate_indexÚprefer_currentrþ  s             @r)   Ú_dedupe_replayed_userz*SessionMessagesMixin._dedupe_replayed_user>  sÓ   ø€ ð
 !×AÑAÀ#ÓFÀqÑIÐØ×=Ñ=¸c¿g¹gÀkÓ>RÐTeÓfˆØCRÐC^Ð*×.Ñ.¨Ô?ÐdhˆØˆ	ØÐ%Ü!Ó"l¼À8Ó9LÔ"lÐnrÓsˆNØÐ)Ø+¨TÐ2�	ØÐØ×BÑBÀ8ÈSÓQˆIØÐØ˜/Ð)Ð)Ø*3Ñ'ˆ˜ÙØ�L‰L˜Ô)Ø!Ð! ?Ð2Ð2r1   )r÷  rø  c               óH  ‡— ddl m}m} g }	i }
|D �]T  Š| j                  ‰d   | j	                  ‰d   «      «      }d‰d   d|t
        di}|r‰d   �‰d   |d<   |j                  ˆfd	„d
D «       «       ‰d   r| j                  ‰d   «      x}�||d<   |r
‰d   rd|d<   |j                  ˆfd„dD «       «       ‰d   rt        ‰d   g d«      |d<   ‰d   r‰d   |d<   ‰d   rd|d<   ‰d   dk(  rA|j                  ˆfd„dD «       «       ‰d   �‰d   |d<   |j                  ˆfd„dD «       «       |r!| j                  |	||
«      \  }}|r�Œ=|�||
|<   |	j                  |«       �ŒW  | ||	«      «      }	|r*|	r(ddlm}  |d|	«      }|rt        j                  d||«       |	S )a³  Decode fetched rows (ordered by id, pre-filtered) into OpenAI format, stable key order. Every dict is
        stamped ``_DB_PERSISTED_MARKER_KEY`` (born durable) so an identity-losing handoff never re-appends the
        transcript on flush. ``_row_id`` is opt-in (gateway reactions); reasoning restored on assistant rows
        only; ``api_content`` VERBATIM (no sanitize/strip) so replay keeps the provider prompt cache byte-stable.r   )Ú _strip_background_review_harnessÚ_strip_stale_tool_call_markersrž   rf   Tr   Nr(  c              3  ó8   •K  — | ]  }‰|   sŒ	|‰|   f–— Œ y ­wr’   r»   ©rQ   ÚcolrH   s     €r)   rS   z=SessionMessagesMixin._rows_to_conversation.<locals>.<genexpr>j  s"   øè ø€ Ò^¨3ÐUXÐY\ÓU]˜˜S ™X”Ñ^ùó   ƒ
Ž)r¯   r�   rq   r®   c              3  ó8   •K  — | ]  }‰|   sŒ	|‰|   f–— Œ y ­wr’   r»   r  s     €r)   rS   z=SessionMessagesMixin._rows_to_conversation.<locals>.<genexpr>o  s)   øè ø€ ò yØ$'ÐorÐsvÓow��c˜#‘h”ñyùr  )rŸ   rœ   r›   r    r6   zKFailed to deserialize tool_calls in conversation replay, falling back to []r«   r¬   r­   r  c              3  ó8   •K  — | ]  }‰|   sŒ	|‰|   f–— Œ y ­wr’   r»   r  s     €r)   rS   z=SessionMessagesMixin._rows_to_conversation.<locals>.<genexpr>y  s"   øè ø€ Òa¨sÐX[Ð\_ÓX`˜C  S¡œ?Ñaùr  )r¢   r£   r¤   c           	   3  óT   •K  — | ]  }‰|   r|t        ‰|   d d|› d�«      f–— Œ! y ­w)NzFailed to deserialize z, falling back to None)r*   r  s     €r)   rS   z=SessionMessagesMixin._rows_to_conversation.<locals>.<genexpr>|  sA   øè ø€ ò ràÐhkÐloÒhpð œ( 3 s¡8¨TÐ5KÈCÈ5ÐPfÐ3gÓhÔiñrùs   ƒ%(r§   )Úrepair_message_sequenceu…   Repaired %d message-alternation violation(s) while restoring session %s â€” durable transcript kept them, see repair_message_sequence)r   r  r  rL  rj   Ú_DB_PERSISTED_MARKER_KEYr²  rx   r*   r
  rü   Úagent.agent_runtime_helpersr  r%   Úinfo)rY   rZ  r[   rõ  rö  r÷  rø  r  r  râ   r  rf   rš   Údecodedra  r  r  ÚrepairedrH   s                     @r)   rù  z*SessionMessagesMixin._rows_to_conversationT  s'  ø€ ÷ 	bØˆØCEÐØó '	!ˆCØ×/Ñ/°°F±¸T×=QÑ=QÐRUÐV_ÑR`Ó=aÓbˆGð ˜3˜v™;¨	°7Ô<TÐVZÐ[ˆCñ  3 t¡9Ð#8Ø!$ T¡��I‘Ø�J‰JÓ^Ð2QÔ^Ô^ØÐ%Ò&°t×7TÑ7TÐUXÐYkÑUlÓ7mÐ,m¨GÐ+zØ*1�Ð&Ñ'Ù&¨3Ð/DÒ+EØ-1�Ð)Ñ*Ø�J‰Jó yØ+kôyô yà�<Ò Ü$,Ø˜Ñ% rÐ+xó%z��LÑ!àÐ(Ò)Ø$'Ð(=Ñ$>��LÑ!Ø�:ŠØ"&��J‘Ø�6‰{˜kÒ)Ø—
‘
ÓaÐ6TÔaÔaØÐ*Ñ+Ð7Ø/2Ð3FÑ/G�CÐ+Ñ,Ø—
‘
ó ràdôrô rñ !Ø(,×(BÑ(BÀ8ÈSÐRcÓ(dÑ%��oÙÙØ"Ð.Ø9<Ð% oÑ6Ø�O‰O˜CÖ ðO'	!ñT 2Ñ2RÐS[Ó2\Ó]ˆÙ¡(ÝKÙ.¨t°XÓ>ˆHÙÜ—‘ð 2à3;¸ZôIð ˆr1   c                ó  — | j                  | j                  |«      t        d¬«      }| j                  |D �cg c]  }|d   |k(  sŒ|d   sŒ|‘Œ c}|dddd¬«      }| j                  | j	                  |«      |ddd¬«      }||fS c c}w )u/  ``(model_history, display_history)`` for a resume from ONE SELECT; byte-identical to the separate
        reads. model: the tip's active rows, alternation-repaired, summary marker kept for pre-compress
        checkpointing. display: the full lineage (``/branch`` stands alone), compaction-archived rows deduped.

        The display projection also includes rows preserved by IN-PLACE compaction (``active=0,
        compacted=1``), deduped by :meth:`_dedupe_display_generations`. Without them a compacted
        conversation resumes showing only its summary plus the carried-forward tail â€” the user's own turns
        read as deleted even though every row is still on disk, and the REST transcript read (which has
        always included them) disagreed with this one about the same session (#92080).
        Tró  r[   r^  Frô  ©r[   rõ  rö  r÷  )rñ  r	  r  rù  r˜  )rY   r[   rZ  ró   Úmodel_historyÚdisplay_historys         r)   Úget_resume_conversationsz-SessionMessagesMixin.get_resume_conversations’  s´   € ð ×,Ñ,Ø×$Ñ$ ZÓ0Ô2HÐZ^ð -ó `ˆð ×2Ñ2ØÖN�1  ,¡°:Ó =À!ÀHÃ+ŠQÒNÐ[eØ#¸ÈdÐlpð 3ó rˆð ×4Ñ4Ø×,Ñ,¨TÓ2¸zØ"°uÈdð 5ó Tˆð ˜oÐ-Ð-ùò Os   ·BÁBÁBc                óL   — | j                  |«      r|gS | j                  |«      S )zÉSession ids a display resume materializes: the compression lineage, or the session alone for an
        explicit ``/branch`` copy. Shared with the resume guard so it counts exactly what a resume loads.)Ú_is_explicit_branch_sessionÚ_session_lineage_root_to_tiprT  s     r)   r	  z(SessionMessagesMixin._resume_lineage_ids¨  s+   € ð  $×?Ñ?À
ÔK�
ˆ|Ð~ÐQU×QrÑQrÐs}ÓQ~Ð~r1   c                ó6   — |r|gdfS | j                  |«      dfS )zW``tip_only``: the tip's ACTIVE rows (model restore); else the full-lineage DISPLAY set.z
active = 1z(active = 1 OR compacted = 1))r	  )rY   r[   Útip_onlys      r)   Ú_resume_count_scopez(SessionMessagesMixin._resume_count_scope­  s)   € áØ�< Ð-Ð-Ø×'Ñ'¨
Ó3Ð5TÐTÐTr1   )r$  c               ó˜   — | j                  ||«      \  }}t        | j                  dt        |«      › d|› �t	        |«      «      d   «      S )zHCount the rows a resume would materialize (see ``_resume_count_scope``).z3SELECT COUNT(*) FROM messages WHERE session_id IN (ú) AND r   )r%  r`   r  r   r  )rY   r[   r$  r×  r´  s        r)   Úget_resume_message_countz-SessionMessagesMixin.get_resume_message_count³  s[   € à%)×%=Ñ%=¸jÈ(Ó%SÑ"ˆ�]Ü�4—>‘>ØAÄ-ÐP[ÓB\ÐA]Ð]cÐdqÐcrÐsÜ�+Óó à !ñ#ó $ð 	$r1   c          	     ó  — ddl m}m} |€ |«       }|dk  rt        d«      ‚|dk(  ry| j	                  ||«      \  }}t        | j                  dt        |«      › d|› d�g |¢|dz   ‘­«      d   «      }||kD  r ||||rd¬
«      ‚d	¬
«      ‚|S )a5  Resume row count, or raise ``SessionResumeTooLargeError``. ``max_messages=None`` reads config; 0
        disables the guard without counting. ``tip_only`` bounds only the tip's active rows for callers that
        never materialize the lineage: a heavily compressed conversation is a success, not a rejection.r   )ÚSessionResumeTooLargeErrorÚresolved_max_resume_messagesz!max_messages must be non-negativezBSELECT COUNT(*) FROM (SELECT 1 FROM messages WHERE session_id IN (r'  z	 LIMIT ?)r   zin its tip segmentzacross its lineage)Úscope)r   r*  r+  rc   r%  r`   r  r   )	rY   r[   Úmax_messagesr$  r*  r+  r×  r´  Úmessage_counts	            r)   Úassert_resume_safez'SessionMessagesMixin.assert_resume_safeº  sØ   € ÷ 	ZØÐÙ7Ó9ˆLØ˜!ÒÜÐ@ÓAÐAØ˜1ÒØØ%)×%=Ñ%=¸jÈ(Ó%SÑ"ˆ�]Ü˜DŸN™Nð ,;Ü;HÈÓ;UÐ:Vð WØ �/ ð,,à-M¨{Ð-M¸LÈ1Ñ<LÑ-MóOàOPñRó Sˆð ˜<Ò'Ù,Ø˜|É8Ð3Gôoð oØYmôoð oàÐr1   c                óÜ  — | j                  |«      }t        |«      dk  rg S | j                  | j                  |t        d¬«      «      }|D �ch c]  }|d   |k7  sŒ|d   €Œt        |d   «      ’Œ! }}|sg S | j                  ||ddd¬«      }|D ���	cg c]?  }|j                  d«      |v r*|j                  «       D ��	ci c]  \  }}	|dk7  sŒ||	“Œ c}	}‘ŒA c}	}}S c c}w c c}	}w c c}	}}w )	zèAncestor-only display messages of a lineage (row ``session_id != tip``) that ``session.resume``
        prepends. Identified by row origin, not ``display[:len(display) - len(model)]``, so alternation
        repair cannot overcount.r   Tró  r[   r   Fr  r(  )	r	  r:   r˜  rñ  r  r`   rù  r—   r5  )
rY   r[   r×  rZ  rH   Úancestor_idsr  ÚmessagerR   r“   s
             r)   Úget_ancestor_display_prefixz0SessionMessagesMixin.get_ancestor_display_prefixÎ  s  € ð ×.Ñ.¨zÓ:ˆÜˆ{Ó˜qÒ ØˆIØ×/Ñ/Ø×)Ñ)¨+Ô7MÐ_cÐ)Ódófˆà26Öt¨3¸#¸lÑ:KÈzÓ:YÐ^aÐbfÑ^gÑ^sœ˜C ™I�ÐtˆÐtÙØˆIØ×,Ñ,Ø˜Z¸4ÐTYÐkoð -ó qˆð #÷Nð NØ g§k¡k°)Ó&<ÀÑ&Lð #*§-¡-£/×D™$˜!˜Q°Q¸)³^��A‘ÕDô Nð 	Nùò uùó
 Eùô Ns0   ÁCÁCÁ!CÂ+C'Â>C!ÃC!ÃC'Ã!C'c                ó@   — | j                  |«      }|r
|d   r|d   S |S )z¶ROOT id of the lineage: the stable conversation id across compression segments and delegate
        subagents (Nous Portal usage tagging). Unchanged when there is no recorded parent.r   )r"  )rY   r[   Úchains      r)   Úget_conversation_rootz*SessionMessagesMixin.get_conversation_rootß  s+   € ð ×1Ñ1°*Ó=ˆÙ  U¨1¢Xˆu�Q‰xÐ=°:Ð=r1   c                ó¬   — | j                  d«      dk7  ryt        | «      \  }}|duxr |du}|r|j                  d«      |fS | j                  d«      |fS )z=Return canonical live content and whether *msg* is composite.rž   r×   )NFNrf   )r—   r   )rš   r‰  rŠ  Úis_composites       r)   rÿ  z5SessionMessagesMixin._canonical_replayed_user_contentå  se   € ð �7‰7�6‹?˜fÒ$ØÜ7¸Ó<Ñˆ�Ø dÐ*ÒD¨yÀÐ/DˆÙ+7ˆy�}‰}˜YÓ'ÐQ]Ð]Ð]¸S¿W¹WÀYÓ=OÐQ]Ð]Ð]r1   c                óx   — | �|ddg fv ry	 | t        j                  |ddd¬«      fS # t        t        f$ r Y yw xY w)z8Return a hashable key for a column-exact rotation clone.NrO   FT)ú,ú:)Úensure_asciiÚ	sort_keysÚ
separators)r!   rb   r$   rc   )rŸ   rf   s     r)   r   z3SessionMessagesMixin._exact_replayed_user_clone_keyî  sR   € ð Ð ¨D°"°b¨>Ñ 9Øð	ØœdŸj™j¨¸uÐPTÐakÔlÐlÐløÜœ:Ð&ò 	Ùð	ús   Œ' §9¸9c                ó¬  — |j                  d«      dk7  ryt        j                  } ||«      \  }}|ddg fv ryt        t	        | «      dz
  dd«      D ]€  }| |   }|j                  d«      dk(  r, ||«      \  }}||k(  sŒ-|s|st        |t        «      sŒB||fc S |j                  d«      dk(  sŒ]|j                  d«      s|j                  d	«      sŒ€ y y)
a;  Adjacent replay duplicate ``(index, prefer_current)`` or None. Rotation may persist the current ask
        in the parent and again inside a composite child carrier: carriers compare by canonical live payload,
        ordinary users by exact string. The child carrier wins (it owns the durable row id and scaffold).rž   r×   NrO   r   r®  r  rf   r6   )r—   rL   rÿ  ru   r:   r3   r4   )	râ   rš   Ú	canonicalrf   r	  ÚindexÚprevÚprev_contentÚprev_is_composites	            r)   r  z:SessionMessagesMixin._find_duplicate_replayed_user_messageø  sß   € ð �7‰7�6‹?˜fÒ$ØÜ(×IÑIˆ	Ù"+¨C£.Ñˆ�Ø�t˜R �nÑ$ØÜœ3˜x›=¨1Ñ,¨b°"Ó5ò 	ˆEØ˜E‘?ˆDØ�x‰x˜Ó 6Ò)Ù2;¸D³/Ñ/�Ð/Ø 7Ó*±ÑBSÔWaÐbiÔknÕWoØ  .Ð0Ò0Ø—‘˜&Ó! [Ó0°d·h±h¸yÔ6IÈTÏXÉXÐVbÕMcÙð	ð r1   c                ón   — | j                  t        |f«      D �cg c]  }t        |d   «      ‘Œ c}S c c}w )zbOrdered physical active ids for rewind CAS checks (includes legacy harness rows projections omit).r   )rÐ  Ú_ACTIVE_IDS_SQLr`   r"  s      r)   Úget_active_message_idsz+SessionMessagesMixin.get_active_message_ids  s+   € à'+§~¡~´oÈ
À}Ó'UÖV ”�C˜‘F•ÒVÐVùÒVs   š2c                ó~   — | j                  d|f«      j                  «       }t        |«      t        d„ |D «       «      fS )z?Return active message/tool-call counts inside the caller's txn.zCSELECT tool_calls FROM messages WHERE session_id = ? AND active = 1c              3  ó<   K  — | ]  }t        |d    d¬«      –— Œ y­w)r   r   r<  Nr=  r>  s     r)   rS   zASessionMessagesMixin._active_transcript_counts.<locals>.<genexpr>  s   è ø€ ÒPÀCœo¨c°!©f¸Q×?Ð?ÑPùr?  )rV   r  r:   rë   )rZ   r[   rZ  s      r)   Ú_active_transcript_countsz.SessionMessagesMixin._active_transcript_counts  s<   € ð �|‰|ÐaÐdnÐcpÓq×zÑzÓ|ˆÜ�4‹yœ#ÑPÈ4ÔPÓPÐPÐPr1   c           	     ó¸  — |j                  d«      st        d«      ‚t        i |¥| j                  |j                  d«      «      | j	                  |j                  d«      «      dœ¥«      \  }}|€t        d«      ‚|j                  d«      }t        |t        «      rt        |«      j                  «       }|�||k7  rt        d«      ‚|r|€t        d	«      ‚|r|S dS )
zÄValidate an active rewind target; return its handoff scaffold (or None). ``ValueError``: inactive /
        non-user-originated / missing composite carrier; ``RuntimeError``: live payload changed.r^  zrewind target is not activerf   rq   )rf   rq   Nz+rewind target is not a user-originated turnz2rewind target changed before it could be persistedz@preserve_compaction_handoff requires an active composite carrier)
r—   rc   r   rj   rx   r3   r4   r   rP   ÚRuntimeError)rY   Ú
target_rowÚexpected_target_contentÚpreserve_compaction_handoffr‰  rŠ  Úlive_contents          r)   Ú_split_rewind_targetz)SessionMessagesMixin._split_rewind_target  sé   € ð �~‰~˜hÔ'ÜÐ:Ó;Ð;Ü7ð 9cØð9cØ%)×%9Ñ%9¸*¿.¹.ÈÓ:SÓ%TØ $× =Ñ =¸j¿n¹nÐM_Ó>`Ó aò9có dÑˆ�ð ÐÜÐJÓKÐKØ —}‘} YÓ/ˆÜ�l¤CÔ(Ü+¨LÓ9×?Ñ?ÓAˆLØ"Ð.°<ÐCZÒ3ZÜÐSÓTÐTÙ&¨7¨?ÜÐ_Ó`Ð`Ù5ˆwÐ?¸4Ð?r1   )rO  Úexpected_active_idsrN  c               óÄ   ‡ ‡‡‡‡‡— ˆˆˆˆ ˆˆfd„}‰ j                  |«      \  }}}	}
‰ j                  |j                  d«      «      |d<   t        |«      ||	dœ‰rd|
i¥S i ¥S )aê  Soft-delete (``active=0``) every message with id >= *target_message_id*, target included (the caller
        pre-fills it as the next prompt). Returns ``{"rewound_count", "target_message", "new_head_id"}``, plus
        ``replacement_message_id`` with ``preserve_compaction_handoff`` (archives a composite summary carrier,
        inserts its hidden handoff scaffold as the new head). ``ValueError``: target missing or not ``user``.
        ``expected_active_ids`` / ``expected_target_content`` pin the active set and canonical live payload
        in-txn before any mutation (presentation-only metadata changes don't invalidate a rewind). A live turn
        lease refuses; expired/dead holders are reclaimed. ``rewind_count`` always increments.c                ó
  •— ‰j                  | ‰d dd¬«       ‰�O| j                  t        ‰f«      j                  «       }|D �cg c]  }t	        |d   «      ‘Œ c}‰k7  rt        d«      ‚| j                  d‰‰f«      j                  «       }|€t        d‰› d‰› �«      ‚t        |«      }|j                  d«      d	k7  r!t        d
|j                  d«      ›d‰› d�«      ‚d x}}‰s‰�‰j                  |‰‰«      }| j                  d‰‰f«      j                  «       D �cg c]  }|d   ‘Œ	 }}|r| j                  dt        |«      › d�|«       |�?‰j                  | ‰|g«       t	        | j                  d«      j                  «       d   «      }| j                  d‰f«       ‰j                  | ‰«      \  }}	| j                  t        › d�||	‰f«       | j                  d‰f«      j                  «       d   }
|||
|fS c c}w c c}w )NTr9  r   z>active transcript changed before the rewind could be persistedz6SELECT * FROM messages WHERE id = ? AND session_id = ?zmessage z not found in session rž   r×   z1rewind target must be a 'user' message (got role=z, id=r   zGSELECT id FROM messages WHERE session_id = ? AND id >= ? AND active = 1z,UPDATE messages SET active = 0 WHERE id IN (zSELECT last_insert_rowid()zMUPDATE sessions SET rewind_count = COALESCE(rewind_count, 0) + 1 WHERE id = ?r½   z@SELECT MAX(id) FROM messages WHERE session_id = ? AND active = 1)r�   rV   rF  r  r`   rL  rW   rc   rm   r—   rQ  r   rè   rJ  rD  )rZ   Úactive_rowsró   rH   rM  Úreplacement_message_idÚreplacementr  r.  Útool_call_countÚhead_idrR  rN  rO  rY   r[   Útarget_message_ids              €€€€€€r)   rË   z3SessionMessagesMixin.rewind_to_message.<locals>._do8  sZ  ø€ Ø×/Ñ/Ø�j $ÀÐfjð 0ô là"Ð.Ø"Ÿl™l¬?¸Z¸MÓJ×SÑSÓU�Ø'2Ö3 !”C˜˜!™•IÒ3Ð7JÒJÜ&Ð'gÓhÐhØ—,‘,ØHÐK\Ð^hÐJiókßksÑksÓkuð àˆ{Ü  8Ð,=Ð+>Ð>TÐU_ÐT`Ð!aÓbÐbÜ˜c›ˆJØ�~‰~˜fÓ%¨Ò/Ü ØGÈ
ÏÉÐW]ÓH^ÐGaÐafÐgxÐfyÐyzÐ{ó}ð }à37Ð7Ð" [Ù*Ð.EÐ.QØ"×7Ñ7¸
ÐD[Ð]xÓy�Ø!%§¡Ð.wØ.8Ð:KÐ-Ló"NßNVÉhËjöZ˜A�1�Q“4ð ZˆCð ZáØ—‘ÐKÌMÐZ]ÓL^ÐK_Ð_`ÐaÐcfÔgØÐ&Ø×)Ñ)¨$°
¸[¸MÔJÜ),¨T¯\©\Ð:VÓ-W×-`Ñ-`Ó-bÐcdÑ-eÓ)fÐ&Ø�L‰LØ_ÐblÐanôpà-1×-KÑ-KÈDÐR\Ó-]Ñ*ˆM˜?Ø�L‰LÔ-Ð.¨mÐ<¸}ÈoÐ_iÐ>jÔkØ—l‘lØRÐU_ÐTaócßckÑckÓcmÐnoñqˆGà˜s GÐ-CÐCÐCùò5 4ùòZs   ÁG;ÄH rf   )Úrewound_countÚtarget_messageÚnew_head_idrV  )rÏ   rj   r—   r:   )rY   r[   rZ  rO  rR  rN  rË   rM  Úrewoundr]  rV  s   ``````     r)   Úrewind_to_messagez&SessionMessagesMixin.rewind_to_message.  s†   ý€ ÷	Dñ 	Dð@ DH×CVÑCVÐWZÓC[Ñ@ˆ
�G˜[Ð*@à $× 4Ñ 4°Z·^±^ÀIÓ5NÓ Oˆ
�9ÑÜ!$ W£ÀÐ\gñ oÙIdÐ,Ð.DÐEðoð 	oØjlðoð 	or1   c                óV   — d|rdndz   }| j                  ||r
|f«      d   S d«      d   S )z2Count messages, optionally for a specific session.zSELECT COUNT(*) FROM messagesr­  rO   r»   r   r!  )rY   r[   r  s      r)   r.  z"SessionMessagesMixin.message_count^  s9   € à-ÉJÑ1HÐ\^Ñ_ˆØ�~‰~˜c±J J =ÓGÈÑJÐJÀBÓGÈÑJÐJr1   c                ó.   — | j                  d||f«      duS )zNProbe the accepted-input marker without allocating message bodies or archives.zèSELECT 1 FROM messages WHERE session_id = ? AND role = 'user' AND observed = 0 AND (active = 1 OR compacted = 1) AND CASE WHEN json_valid(display_metadata) THEN json_extract(display_metadata, '$.gateway_input_owner') END = ? LIMIT 1Nr!  )rY   r[   Úowners      r)   Úhas_gateway_input_ownerz,SessionMessagesMixin.has_gateway_input_ownerc  s-   € à�~‰~ð[ð ˜Ðó!ð
 )-ð-ð 	-r1   c                ó.   — | j                  d||f«      duS )a‚  True when *platform_message_id* exists (partial-index probe; the gateway's transient-failure dedupe).

        Uses the idx_messages_platform_msg_id partial index for efficient lookup. Used by the gateway's
        transient-failure dedupe guard (#47237) to skip re-persisting a user message that was already saved
        on a prior retry of the same inbound platform message.
        zOSELECT 1 FROM messages WHERE session_id = ? AND platform_message_id = ? LIMIT 1Nr!  )rY   r[   r«   s      r)   Úhas_platform_message_idz,SessionMessagesMixin.has_platform_message_idl  s)   € ð �~‰~Ø]ØÐ,Ð-ó/à6:ð;ð 	;r1   )Úinclude_resetc               ó²  — |j                  d«      dk(  ry|j                  d«      }t        |t        «      r	 t        j                  |«      }t        |t        «      sy|j                  d«      |j                  d«      f}|r||j                  d«      fz  }|j                  d	«      }|r||v S t        d
„ |D «       «      S # t        j
                  $ r Y yw xY w)ag  True when *session* is a branch, delegate, or tool child of its parent (``include_reset``: also a
        reset fork). Markers only count when they point at ``parent_session_id``: compression copies
        ``model_config`` onto the continuation, so presence-only matching would misclassify it (same binding
        as ``_NON_CONTINUATION_CHILD_FILTER_SQL``).rT   ÚtoolTÚmodel_configFÚ_branched_fromÚ_delegate_fromÚ_reset_fromÚparent_session_idc              3  ó$   K  — | ]  }|d u–— Œ
 y ­wr’   r»   )rQ   Úms     r)   rS   zCSessionMessagesMixin._is_explicit_fork_child_row.<locals>.<genexpr>Š  s   è ø€ Ò9YÈA¸!À4¼-Ñ9Yùs   ‚)r—   r3   r4   r!   r"   r#   rm   r1  )rY   Úsessionrf  ÚcfgÚmarkersÚ	parent_ids         r)   Ú_is_explicit_fork_child_rowz0SessionMessagesMixin._is_explicit_fork_child_roww  sÏ   € ð
 �;‰;�xÓ  FÒ*ØØ�k‰k˜.Ó)ˆÜ�cœ3ÔðÜ—j‘j “o�ô ˜#œtÔ$ØØ—7‘7Ð+Ó,¨c¯g©gÐ6FÓ.GÐHˆÙØ˜Ÿ™ Ó.Ð0Ñ0ˆGØ—K‘KÐ 3Ó4ˆ	Ù'0ˆy˜GÐ#ÐY´cÑ9YÐQXÔ9YÓ6YÐYøô ×'Ñ'ò Ùðús   ¸C  Ã CÃCc                ó`   — | j                  |«      }t        |xr | j                  |«      «      S )zªRead-only :meth:`_is_explicit_fork_child_row`; a missing row is not a fork (prompt_cache_scope keeps
        a declared conversation key from crossing the fork boundary).)Úget_sessionrƒ  rt  )rY   r[   rp  s      r)   Úis_explicit_fork_childz+SessionMessagesMixin.is_explicit_fork_childŒ  s/   € ð ×"Ñ" :Ó.ˆÜ�GÒI × @Ñ @ÀÓ IÓJÐJr1   c                ót   — |r|sy| j                  d||f«      }|�|d   �t        |d   «      nd}|dkD  r|S dS )a  Conversation boundaries (``_RESET_END_REASONS`` ends) this peer has crossed, or ``None`` if never
        reset. The peer is ``(session_key, source)``, never the key alone (an API caller may legally reuse a
        Telegram row's key). Read from ``conversation_generations`` (advanced in each boundary's txn), not an
        aggregate over session rows: deletes/prunes would re-emit a retired pair. Rows are never GC'd
        (dropping one re-issues generation 1: the ABA this prevents). Wall-clock-free, so a backwards NTP
        correction cannot reorder it. DBs upgraded mid-conversation take their first generation from the
        next boundary written (a pre-upgrade reset shares its predecessor's scope once: costs a warm
        prompt-cache bucket, never crosses an identity).NzTSELECT generation FROM conversation_generations WHERE source = ? AND session_key = ?Ú
generationr   r  )rY   rU   rT   rH   ry  s        r)   Úlatest_conversation_boundaryz1SessionMessagesMixin.latest_conversation_boundary’  sX   € ñ ¡&ØØ�n‰nØbØ�[Ð!ó#ˆð 03¨À3À|ÑCTÐC`”S˜˜\Ñ*Ô+Ðfgˆ
Ø'¨!š^ˆzÐ5°Ð5r1   c                ó2   ‡— ˆfd„}| j                  |«       y)z9Delete all messages for a session and reset its counters.c                óZ   •— | j                  d‰f«       | j                  t        ‰f«       y )Nr@  )rV   Ú_RESET_COUNTERS_SQL)rZ   r[   s    €r)   rË   z0SessionMessagesMixin.clear_messages.<locals>._do¥  s%   ø€ Ø�L‰LÐDÀzÀmÔTØ�L‰LÔ,¨z¨mÕ<r1   Nrî   )rY   r[   rË   s    ` r)   Úclear_messagesz#SessionMessagesMixin.clear_messages£  s   ø€ ô	=ð 	×Ñ˜CÕ r1   )Údry_runÚbackupc               ó¤  ‡‡
‡— ddl mŠ
 dˆ
fd„Šdˆfd„	}| j                  «       5 } ‰|«      }ddd«       ‰ss |«      S d}|r²ddl}|j                  j	                  «       j                  d«      }t        | j                  j                  | j                  j                  › d|› �«      «      }| j                  5  | j                  j                  d|f«       ddd«       t        j                  d	|«       ˆfd
„}	| j                  |	«      }|rt        j                  dt!        |«      «        |||«      S # 1 sw Y   �ŒxY w# 1 sw Y   ŒlxY w)a  Permanently clear bare tool-call marker content ("[memory]") left by pre-fix sessions
        (``_rows_to_conversation`` repairs it in memory; this stops the re-scan). Only ``content`` is touched.
        ``backup``: ``VACUUM INTO`` snapshot first (none when nothing changes).r   )Ú_STALE_TOOL_CALL_MARKER_REc                óÜ   •— | j                  d«      }|j                  «       D �cg c]<  }t        |d   t        «      r'‰j	                  |d   j                  «       «      r|d   ‘Œ> c}S c c}w )NziSELECT id, content FROM messages WHERE role = 'assistant' AND tool_calls IS NOT NULL AND tool_calls != ''rf   r   )rV   r  r3   r4   Ú	fullmatchrP   )rZ   ÚcursorrH   r‚  s      €r)   Ú_find_affectedzJSessionMessagesMixin.purge_stale_tool_call_markers.<locals>._find_affected¯  sr   ø€ Ø—\‘\ð #[ó \ˆFà)/¯©Ó):ö u #Ü˜c )™n¬cÔ2Ð7Q×7[Ñ7[Ð\_Ð`iÑ\j×\pÑ\pÓ\rÔ7sð ˜“Iò uð uùò us   ¥AA)Nc                ó$   •— ‰t        | «      | |dœS )N)r  Úrows_affectedÚrow_idsÚbackup_path)r:   )ÚaffectedrŠ  r  s     €r)   Ú_resultzCSessionMessagesMixin.purge_stale_tool_call_markers.<locals>._result´  s   ø€ Ø&¼¸X»ÐS[ÐlwÑxÐxr1   z%Y%m%d_%H%M%Sz.pre-clean-markers-backup-zVACUUM INTO ?z3Backed up state.db to %s before clean-markers writec                óZ   •—  ‰| «      }|r| j                  dt        |«      › d�|«       |S )Nz.UPDATE messages SET content = '' WHERE id IN (r   )rV   r   )rZ   r  r†  s     €r)   rË   z?SessionMessagesMixin.purge_stale_tool_call_markers.<locals>._doÂ  s6   ø€ Ù  Ó&ˆCÙØ—‘ÐMÌmÐ\_ÓN`ÐMaÐabÐcÐehÔiØˆJr1   zIPermanently cleared %d stale tool-call marker row(s) in state.db (#78148))Úreturnú	List[int]r’   )r   r‚  r¦  ÚdatetimerI   Ústrftimer4   Údb_pathÚ	with_nameÚnameÚ_lockÚ_connrV   r%   r  rÏ   r:   )rY   r  r€  rŒ  rZ   Úaffected_idsrŠ  r�  ÚstamprË   r‚  r†  s    `        @@r)   Úpurge_stale_tool_call_markersz2SessionMessagesMixin.purge_stale_tool_call_markersª  s-  ú€ õ 	<õ	uõ
	yà�^‰^Óð 	0 Ù)¨$Ó/ˆL÷	0á™,Ù˜<Ó(Ð(Ø%)ˆÙÛØ×%Ñ%×)Ñ)Ó+×4Ñ4°_ÓEˆEÜ˜dŸl™l×4Ñ4¸¿¹×8IÑ8IÐ7JÐJdÐejÐdkÐ5lÓmÓnˆKØ—‘ñ DØ—
‘
×"Ñ" ?°[°NÔC÷Dä�K‰KÐMÈ{Ô[ô	ð
 ×*Ñ*¨3Ó/ˆÙÜ�K‰KÐcÔehÐiuÓevÔwÙ�| [Ó1Ð1÷)	0ñ 	0ú÷Dð Dús   ¦	D9Â=EÄ9EÅE)r[   r4   r\   r4   rŽ  ÚNone)rf   r   rŽ  r   )rq   r   rŽ  úOptional[str])r'   r   rŽ  úOptional[Dict[str, Any]])r-   r   rŽ  r›  )Nç     Àr@FFF)r[   r4   r†   r›  r‡   r›  rˆ   rG   r‰   rƒ  rŠ   rƒ  r‹   rƒ  rŽ  rš  )r[   r4   rž   r4   rš   úDict[str, Any]r6   r   r²   rG   r™   rƒ  rŽ  r  )
r[   r4   r¾   r`   r6   r`   r¿   rƒ  rŽ  rš  )NNNNNNNNNNNNFNFNNNNNNr�  )2r[   r4   rž   r4   rf   r4   r›   r4   r6   r   rœ   r4   r¡   r`   r¢   r4   r£   r4   r¤   r4   r¨   r   r©   r   rª   r   r«   r4   r­   rƒ  r    r›  r®   rƒ  rŸ   r   r¯   r›  r�   r›  rq   rœ  r†   r›  r‡   r›  rˆ   rG   rŽ  r`   )r[   r4   rf   r4   rÛ   rž  rŽ  r`   )NNNr�  )r[   r4   râ   úList[Dict[str, Any]]r†   r›  r‡   r›  rá   úOptional[int]rˆ   rG   rŽ  r`   )r[   r4   rž   r4   rf   r4   r�   r4   rq   rœ  rŽ  rƒ  )rv   rœ  rŽ  rŸ  )
r[   r4   r   r`   rù   r›  rõ   r4   rŽ  zOptional[List[Dict[str, Any]]])r[   r4   r   r`   rŽ  rŸ  )r[   r4   r   r`   rŽ  zTuple[str, tuple])r[   r4   rõ   r4   rŽ  rŸ  )
r[   r4   rž   r4   r  r`   r  rƒ  rŽ  r   )r[   r4   rŽ  r›  )r[   r4   r  r`   rŽ  r›  )r[   r4   râ   rŸ  rŽ  útuple[int, int])r[   r4   r3  r`   rŽ  r`   )FFF)r[   r4   râ   rŸ  rH  rƒ  rI  rƒ  r‰   rƒ  rŽ  rš  )
rž   r4   rf   r   rœ   r   r6   r   rŽ  r  )rF  r9   râ   rŸ  rŽ  r`   )rž   r4   rf   r   rŽ  r   )r[   r4   rŽ  rƒ  )r[   r4   rŽ  r`   )r  r4   rŽ  zTuple[List[int], int])re  r�  r[   r›  rŽ  rš  )NNNr   )r[   r4   rv  rŸ  rx  rœ  rz  r   rw  r›  ry  r`   rŽ  r`   )rŽ  ú	List[str])r[   r4   rf   r   r¯   r4   rŽ  r`   )
r[   r4   r  r`   rf   r   r¯   r4   rŽ  r`   )r[   r4   r  r`   rf   r   rŽ  r`   )rŽ  úTuple[Any, ...])r˜   r£  rŽ  r_   )r[   r4   r´  r4   rµ  r   r  r`   r¶  rƒ  rŽ  z	List[Any])rÄ  r4   rÅ  rƒ  rŽ  rž  )rÈ  rƒ  rÉ  rƒ  rŽ  r4   )FFNr   FN)r[   r4   rÈ  rƒ  rÉ  rƒ  rµ  r   r  r`   r¶  rƒ  rÓ  r   rŽ  rŸ  )r×  r¢  rŽ  rŸ  )é   )r[   r4   rá  r`   rÜ  r`   rŽ  rž  )r[   r4   rŽ  r4   )r×  r¢  r´  r4   rð  rƒ  )FFFFF)r[   r4   rõ  rƒ  rÈ  rƒ  rö  rƒ  r÷  rƒ  rÉ  rƒ  rŽ  rŸ  )rŽ  zTuple[bool, Any])r[   r4   rõ  rƒ  rö  rƒ  r÷  rƒ  rø  rƒ  rŽ  rŸ  )r[   r4   rŽ  z1Tuple[List[Dict[str, Any]], List[Dict[str, Any]]])r[   r4   rŽ  r¢  )r[   r4   r$  rƒ  rŽ  zTuple[List[str], str])r[   r4   r$  rƒ  rŽ  r`   r’   )r[   r4   r-  r   r$  rƒ  rŽ  r`   )r[   r4   rŽ  rŸ  )rš   rž  rŽ  zTuple[Any, bool])rŸ   r   rf   r   rŽ  zOptional[Tuple[Any, str]])râ   rŸ  rš   rž  rŽ  zOptional[Tuple[int, bool]])r[   r4   rŽ  r�  )r[   r4   rŽ  r¡  )rM  rž  rN  r   rO  rƒ  )r[   r4   rZ  r`   rO  rƒ  rR  zOptional[List[int]]rN  r   rŽ  rž  )r[   r4   rb  r4   rŽ  rƒ  )r[   r4   r«   r4   rŽ  rƒ  )rp  rž  rf  rƒ  rŽ  rƒ  )rU   r4   rT   r4   rŽ  r   )r[   r4   rŽ  rš  )r  rƒ  r€  rƒ  rŽ  rž  )Oro   Ú
__module__Ú__qualname__Ú__doc__r]   Úclassmethodrg   rj   Ústaticmethodrr   rx   rz   r�   r¸   rÂ   rÑ   rÜ   rß   rï   rô   r  r  r  r  r  r#  r%  rè   r)  rJ  rM  rC  rL  rU  rW  r[  rh  r{  rd  r�  r„  r†  r±   r°   r˜  r©  rÀ  rÆ  rÊ  rÎ  rÚ  rß  rí  rñ  rú  r
  rù  r  r	  r%  r(  r/  r3  r6  rÿ  r   r  rG  rJ  rQ  r_  r.  rc  re  rt  rw  rz  r~  r™  r»   r1   r)   rL   rL   r   s2  „ ÙXóFð ò6ó ð6ð òó ðð ò,ó ð,ð" òó ðð  ò_ó ð_ð RWØW\Ø05ð7<Ø(ð7<ØINð7<à"&ð7<àPTð7<ð *.ð7<ð ;?ó7<ðr^Ø ð^Ø59ð^Ø>Có^ð8 ò
ró ð
rð imØgkØcgØ[`ØfjØIMØdhØQVðVØðVØ%(ðVØ36ðVØJMðVØbeðVàðVà/2ðVàJMðVàadðVð ðVð ;>ðVð ^aðVð !ð	Vð ?Bð	Vð UYð	Vð
 *ðVð
 HLðVð
 adðVð #ðVð :GðVð 3ðVð UbðVð )ðVð JOðVð \_óVó: VðF imØMQØ(-ðVØðVØ)=ðVØXeðVà(ðVà=JðVð !&ðVð 36óVð4 _cñ(Ø?Bð(àC[ð(àgkó(ó&dð ,2ñ(Ø%(ð(Ø6Tó(ó4eódð GMõ  (ðD EKÐZ[Ø37ñ
'Ø,0ð
'Ø<Ió
'ó'ó'ó*ó4ð  ejØHMð+!Øð+!ØAEð+!ØRVó+!ðZ ò@ó ð@óð" òó ðómópóað
 ]aõ ?ð  Y]Ø=>ð<(Ø4ð<(ØHUð<(à"ð<(à7:ð<(àCFó<(ó|+óYð.aØðaØ'*ðaØ58ðaØGJðaà	óaó6	AóJð òSó ðSòMó +.ðZ&-Ø%)ð&-Ø.7ó&-óPð0 òpó ðpð glØRWØ/3ð-pØ)ð-pØ:=ð-pØKOð-pà,ð-pà8Ló-pó^	ô{ó&'<óR@ð W\Ø`eØ=BØ?Dð8Ø7;ð8ØY]ð8à6:ð8ð 9=ð8ð J^ó8ó$3ð. 7<Ø>Cñ<Ø/3ð<à7;ð<àH\ó<ó|.ó,ó
Uð MRõ $ðÐjoõ ó(Nó">ð ò^ó ð^ð òó ðð ðØ3AðØF`òó ðó0Wð òQó ðQó
@ð& qvØEIØ9=ñ.oØ/Bð.oà36ð.oàBPó.oô`Kó
-ó	;ð ]bõ Zó*Kó6ó"!ð @EÐUYö  2r1   rL   )r'   r   r(   r   r&   r4   rŽ  r   )r-   r   r.   rG   rŽ  rG   )r6   r   rŽ  r   )r6   r   rŽ  r`   )r   )r'   r   r=   r`   rŽ  r`   )r-   r   rŽ  r   )rI   rG   rŽ  rƒ  )Ar§  Ú
__future__r   r�  r!   ÚloggingÚrer�   Útypingr   r   r   r   r   Úagent.context_compressorr	   r  r
   r   r   Úagent.memory_managerr   Úagent.message_sanitizationr   Úhermes_cli.timefmtr   Úhermes_state_commonr   r   r   r   r   r   r   r   Ú	getLoggerr%   rÈ   Ú	frozensetÚfindallÚsplitrÂ  rX   r„   r‚   r  r
  rF  rB  rD  r}  rþ   rn  r0  r2  Úobjectrl   r*   r0   r7   r;   r?   rB   rJ   rL   r»   r1   r)   ú<module>r¸     sH  ðñtõ #ã Û Û Û 	Û ß 3Õ 3÷ ó  õ 2Ý ;Ý +÷?÷ ?ó ?ð 
ˆ×	Ñ	˜>Ó	*€ðcÐ ñ !Ø€B‡J�JˆvÐ*×0Ñ0°°aÓ8¸Ñ;×AÑAÀ#ÀqÓIÈ!ÑLÓMóâ(ñ)Ð ðÐ ð eÐ ØbÐ Ø=Ð ØfÐiÑÐ Ø[€ðPÐ àPÐ Ø_Ð ØOÐ ØiÐ ð"ZÐ àWÐ Ù‹8€óó	,óóaôRóLó
f÷X2ò X2r1   