Ë
    âß²j‰|  ã                  óÆ   — 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mZ ddl	m
Z
mZmZmZmZ e
rddlmZ  ej"                  d«      Z G d„ d	e«      Zdd
„ZdZ G d„ d«      Zy)zëSessionStore transcript I/O: SQLite append with a per-session retry queue, compression-reroute
following, FTS corruption recovery, rewrite/rewind/load. Mixin split out of ``gateway/session.py``;
bound onto ``SessionStore`` via the MRO.é    )ÚannotationsN)Úextract_api_content_sidecar)ÚTYPE_CHECKINGÚAnyÚDictÚListÚOptional)ÚSessionEntryzgateway.sessionc                  ó$   ‡ — e Zd ZdZdˆ fd„Zˆ xZS )ÚTranscriptReadErrorz4Raised when persisted history cannot be read safely.c                ó8   •— || _         t        ‰| �	  d|› �«       y )Nz#transcript read failed for session )Ú
session_idÚsuperÚ__init__)Úselfr   Ú	__class__s     €ú?/home/ubuntu/.hermes/hermes-agent/gateway/session_transcript.pyr   zTranscriptReadError.__init__   s   ø€ Ø$ˆŒÜ‰ÑÐ>¸z¸lÐKÕLó    ©r   ÚstrÚreturnÚNone)Ú__name__Ú
__module__Ú__qualname__Ú__doc__r   Ú__classcell__)r   s   @r   r   r      s   ø„ Ù>÷Mñ Mr   r   c                ó@   — 	 ddl m}  || |«      S # t        $ r Y yw xY w)zŽSpool one evicted/undeliverable message to disk (same machinery as the shutdown flush, so it
    is replayed after DB recovery); path or None.r   )Ú spool_dropped_transcript_messageN)Úgateway.shutdown_flushr   Ú	Exception)r   Úmessager   s      r   Ú_spool_droppedr#      s)   € ðÝKÙ/°
¸GÓDÐDøÜò Ùðús   ‚ ‘	œ)Ú	reasoningÚreasoning_contentÚreasoning_detailsÚcodex_reasoning_itemsÚcodex_message_itemsc                  óZ  — e Zd ZdZdZdZdZd d„Z	 	 	 	 	 	 	 	 d!d„Z	 	 	 	 	 	 	 	 d"d„Z	d„ Z
d#d$d
„Zd%d„Zd&d„Z	 	 	 	 	 	 	 	 d'd„Zd%d„Z	 	 	 	 	 	 	 	 	 	 d(d„Zd)d„Zd*d„Zd+d„Zd,d„Zd*d„Zed-d„«       Zd.d„Zd,d„Zd/d„Zd0d„Z	 	 d1	 	 	 	 	 	 	 	 	 d2d„Zd3d„Zd4d„Z	 d5d	dœ	 	 	 	 	 	 	 d6d„Zy)7ÚSessionTranscriptMixinz›SessionStore transcript I/O: SQLite append with a per-session retry queue,
    compression-reroute following, FTS corruption recovery, rewrite/rewind/load.éÈ   i,  é   c                ó²   — |r| j                  |«      nd}|€|S 	 |j                  |«      xs |S # t        $ r t        j	                  d|d¬«       |cY S w xY w)zŒLatest compression continuation for *session_id* (heals a mapping left pointing at a
        compressed parent by a restart or failed send).Nz,Compression-tip lookup failed for session %sT©Úexc_info)Ú_db_for_session_idÚget_compression_tipr!   ÚloggerÚdebug©r   r   Údbs      r   Ú_compression_tip_for_session_idz6SessionTranscriptMixin._compression_tip_for_session_id<   sg   € ñ 5?ˆT×$Ñ$ ZÔ0ÀDˆØˆ:ØÐð	Ø×)Ñ)¨*Ó5ÒC¸ÐCøÜò 	Ü�L‰LÐGÈÐ^bˆLÔcØÒð	ús   ›0 °#AÁAc                óˆ   — |r|sy|j                   |k7  s||k(  ryt        j                  d|j                   |«       ||_         y)zDRewrite *entry* to the compression continuation if stale. Lock held.Fz8SessionStore healed compressed session mapping: %s -> %sT)r   r2   Úinfo)r   ÚentryÚoriginal_session_idÚcanonical_session_ids       r   Ú_heal_compression_tip_lockedz3SessionTranscriptMixin._heal_compression_tip_lockedH   sO   € ñ #Ñ*>ØØ×ÑÐ2Ò2Ð6JÐNaÒ6aØÜ�‰ØFÈ×HXÑHXØ ô	"ð 0ˆÔØr   c                ó   — |r|r|sy| j                   5  | j                  |«      }|€
	 ddd«       y|j                  |k(  r|cddd«       S | j                  |||«      s
	 ddd«       y| j	                  «        |cddd«       S # 1 sw Y   yxY w)u'  CAS-advance one route along an already-verified compression lineage. Unlike
        ``switch_session`` this never ends/reopens SQLite rows (the compression transaction owns
        that). ``None`` means the route moved after the caller's snapshot (e.g. /new) â€” caller
        must fail closed.N)Ú_lockÚ_entry_lockedr   r<   Ú_save)r   Úsession_keyÚexpected_session_idÚtarget_session_idr9   s        r   Úadvance_compression_sessionz2SessionTranscriptMixin.advance_compression_sessionV   s�   € ñ Ñ"5Ñ=NØØ�Z‰Zñ 		Ø×&Ñ& {Ó3ˆEØˆ}Ø÷		ð 		ð ×ÑÐ#4Ò4Ø÷		ñ 		ð ×4Ñ4°UÐ<OÐQbÔcØ÷		ð 		ð �J‰JŒLØ÷		÷ 		ò 		ús   ”B²BÁBÁ)BÂBc                óB   — | j                  dt        j                  «      S )z?Return the lock that serializes pending-queue drain boundaries.Ú_transcript_drain_lock)Ú_lazyÚ	threadingÚRLock)r   s    r   Ú_get_transcript_drain_lockz1SessionTranscriptMixin._get_transcript_drain_lockj   s   € à�z‰zÐ2´I·O±OÓDÐDr   Fc                ó–   — |ry| j                  «       5  | j                  | j                  |«      |«       ddd«       y# 1 sw Y   yxY w)a&  Serialize transcript draining across queue migration boundaries. A session with no usable
        store is NOT skipped: the write is queued and counted like any other failed append, so a
        dead/unopenable state.db escalates and spools instead of dropping turns silently
        (#114266).N)rJ   Ú _append_to_transcript_serializedÚ_follow_reroutes)r   r   r"   Úskip_dbs       r   Úappend_to_transcriptz+SessionTranscriptMixin.append_to_transcriptn   sK   € ñ
 ØØ×,Ñ,Ó.ñ 	^Ø×1Ñ1°$×2GÑ2GÈ
Ó2SÐU\Ô]÷	^÷ 	^ñ 	^ús	   ”"?¿Ac                ó”   — | j                  dt        «      }t        «       }||v r#||vr|j                  |«       ||   }||v r||vrŒ|S )z5Follow the compression reroute chain (cycle-guarded).Ú_transcript_reroutes)rG   ÚdictÚsetÚadd)r   r   ÚreroutesÚseens       r   rM   z'SessionTranscriptMixin._follow_reroutesx   sX   € à—:‘:Ð4´dÓ;ˆÜ‹uˆØ˜HÑ$¨¸4Ñ)?Ø�H‰H�ZÔ Ø! *Ñ-ˆJð ˜HÑ$¨¸4Ò)?ð Ðr   c                ó°  — | j                   j                  |g «      }|j                  t        |«      «       t	        |«      | j
                  kD  r‡t        ||j                  d«      «      }|�I| j                  dt        «      j                  |«       t        j                  d|| j
                  |«       |S t        j                  d|| j
                  «       |S )zKQueue *message* (retry lock held); evicts + spools the oldest past the cap.r   Ú_spooled_drop_sessionszsSession DB transcript pending queue full for %s (cap=%d); spooled oldest message to %s for replay after DB recoveryzzSession DB transcript pending queue full for %s (cap=%d); dropping oldest message to make room (on-disk spool unavailable))Ú_dirty_transcriptsÚ
setdefaultÚappendrR   ÚlenÚ_MAX_PENDING_PER_SESSIONr#   ÚpoprG   rS   rT   r2   Úwarning)r   r   r"   ÚpendingÚ
spool_paths        r   Ú_enqueue_transcript_messagez2SessionTranscriptMixin._enqueue_transcript_message�   sÀ   € à×)Ñ)×4Ñ4°ZÀÓDˆØ�‰”t˜G“}Ô%ô
 ˆw‹<˜$×7Ñ7Ò7Ü'¨
°G·K±KÀ³NÓCˆJØÐ%Ø—
‘
Ð3´SÓ9×=Ñ=¸jÔIÜ—‘ðAØBLØ×1Ñ1°:ô?ð ˆô	 —‘ðGØHRØ×1Ñ1ô3ð ˆr   c                ó  — t         j                  d|t        |«      j                  |«       | j                  5  t        | j                  j                  |g «      «      }| j                  j                  |d«       | j                  j                  |d«       ddd«       D ]'  }t        ||«      �Œt         j                  d|d¬«       Œ) 	 ddlm}  |||«       y# 1 sw Y   ŒFxY w# t        $ r t         j                  d|d¬«       Y yw xY w)	zºStop SQLite writes on a replaced/quarantined handle and divert the backlog to the on-disk
        spool + JSONL fallback: retrying cannot succeed and the FTS rebuild must never run here.z’Session DB refused further writes on this handle for %s (%s); stopping SQLite writes and diverting pending transcripts to the on-disk fallback: %sNz>pending fallback failed for replaced state.db transcript on %sTr.   r   )Údivert_session_transcript_jsonlz:JSONL divert failed for replaced state.db transcript on %s)r2   ÚerrorÚtyper   Ú_transcript_retry_lockÚlistrY   Úgetr^   Ú_transcript_append_failuresr#   r_   Úhermes_staterd   r!   )r   r   Úqueue_session_idÚexcÚ	remainingÚdroppedrd   s          r   Ú$_divert_transcript_after_db_replacedz;SessionTranscriptMixin._divert_transcript_after_db_replaced˜   s
  € ô
 	�‰ðLØMWÜ�‹I×Ñ ô	%ð ×(Ñ(ñ 	CÜ˜T×4Ñ4×8Ñ8Ð9IÈ2ÓNÓOˆIØ×#Ñ#×'Ñ'Ð(8¸$Ô?Ø×,Ñ,×0Ñ0°¸TÔB÷	Cð !ò 	#ˆGÜ˜j¨'Ó2Ñ:Ü—‘ØTÐV`Ø!ð õ #ð	#ð
	ÝDÙ+¨J¸	ÕB÷	Cð 	Cûô ò 	Ü�N‰NØLÈjØð ö ð	ús   ¸ACÃC' ÃC$Ã'!DÄ
Dc                ó¼   — | j                  |«      }|�|j                  |«      nd}|r4||k7  r/|j                  |«      }|�|j                  d«      €t	        |«      S y)a,  Transitive compression tip of *session_id* if it is a different, still-live row, else ""
        (a depth-1 lookup misses multi-hop lineages). Uses the PARENT's proven owner handle: the
        child's id is unpublished until its write succeeds, so a by-id lookup would hit the ambient
        store.NÚended_atÚ )r0   r1   Úget_sessionri   r   )r   r   Úowner_dbÚtipÚtip_rows        r   Ú_live_compression_childz.SessionTranscriptMixin._live_compression_child²   sf   € ð
 ×*Ñ*¨:Ó6ˆØ:BÐ:Nˆh×*Ñ*¨:Ô6ÐTXˆÙ�3˜*Ò$Ø×*Ñ*¨3Ó/ˆGØÐ" w§{¡{°:Ó'>Ð'FÜ˜3“x�Ør   c                ó¸  — |r|d   |u r|j                  d«       | j                  j                  |g «      }|r!|j                  |«       || j                  |<   n|r|}| j                  j                  |d«       | j                  j                  |d«      }|r3t        || j                  j                  |d«      «      | j                  |<   || j                  |<   |S )ab  Move the retry queue + failure counter from parent to child and record the reroute (retry
        lock held); returns the child's pending list. Older parent backlog must precede messages
        already queued directly on the child; routing is published only AFTER the queue moved
        (caller), so new child writes cannot bypass older parent backlog.r   N)r^   rY   ri   Úextendrj   ÚmaxrQ   )r   r   rl   Úchild_idr`   ÚmsgÚexisting_child_pendingÚprevious_failuress           r   Ú"_migrate_transcript_queue_to_childz9SessionTranscriptMixin._migrate_transcript_queue_to_child¿   s×   € ñ �w˜q‘z SÑ(Ø�K‰K˜ŒNØ!%×!8Ñ!8×!<Ñ!<¸XÀrÓ!JÐÙØ�N‰NÐ1Ô2Ø07ˆD×#Ñ# HÒ-Ù#Ø,ˆGØ×Ñ×#Ñ#Ð$4°dÔ;Ø ×<Ñ<×@Ñ@ÐAQÐSTÓUÐÙÜ9<Ø! 4×#CÑ#C×#GÑ#GÈÐRSÓ#Tó:VˆD×,Ñ,¨XÑ6à08ˆ×!Ñ! *Ñ-Øˆr   c                ó  — | j                   5  | j                  j                  «       D ]  }|j                  |k(  sŒ||_        Œ | j	                  «        ddd«       t        | dd«      xs i j                  |d«       y# 1 sw Y   Œ+xY w)zRRepoint every route at the compression child and save (index authoritative again).NÚ_session_owner_hints)r>   Ú_entriesÚvaluesr   r@   Úgetattrr^   )r   r   r|   r9   s       r   Ú_publish_transcript_reroutez2SessionTranscriptMixin._publish_transcript_rerouteÖ   s|   € à�Z‰Zñ 	ØŸ™×-Ñ-Ó/ò 0�Ø×#Ñ# zÓ1Ø'/�EÕ$ð0ð �J‰JŒL÷		ô
 
�Ð-¨tÓ	4Ò	:¸×?Ñ?ÀÈ$ÕO÷	ð 	ús   �-A>»A>Á>Bc           	     ór  ‡ ‡‡‡‡— ‰ j                   5  ‰ j                  ‰|«      Š‰d   Šddd«       ‰Šdˆˆˆˆ ˆfd„}	 ‰ j                  ‰«       	 ‰ j                  ‰‰«       ‰ j                   5   |«       }|s‰d   Šddd«       ryŒL# 1 sw Y   ŒaxY w# 1 sw Y   ŒxY w# t        $ �rŠ}ddlm}m} ddlm	} t        |||f«      r‰ j                  ‰‰|«       Y d}~yt        ||«      �r‰ j                  ‰«      }	‰ j                  ‰«      }
|
r¥|	r|	‰ j                  dt        «      |
<   	 ‰ j                  |
‰«       ‰ j                   5  ‰ j!                  ‰‰|
‰‰«      Š|
Šddd«       n# 1 sw Y   nxY w‰ j#                  ‰|
«       ‰sY d}~y‰d   Š|
ŠY d}~�Œ`# t        $ r}|}Y d}~nLd}~ww xY w‰ j                   5   |«        ddd«       n# 1 sw Y   nxY wt$        j'                  d‰«       Y d}~y‰ j)                  |«      rj‰ j+                  «       rZ	 ‰ j                  ‰‰«       ‰ j                   5   |«        ddd«       n# 1 sw Y   nxY wY d}~�Œ# t        $ r}|}Y d}~nd}~ww xY w‰ j                   5  ‰ j,                  j/                  ‰d«      dz   }|‰ j,                  ‰<   ddd«       n# 1 sw Y   nxY w‰ j0                  k\  r6‰ j3                  ‰‰«      }t$        j'                  d	‰|t5        ‰«      ||«       n"t$        j7                  d
‰|t5        ‰«      |«       Y d}~yd}~ww xY w)a>  Append a message to a session's transcript (SQLite), draining the per-session retry
        queue.

        Args: skip_db: When True, skip the SQLite write. Used when the agent already persisted messages to
        SQLite via its own _flush_messages_to_session_db(), preventing the duplicate-write bug (#860).
        r   Nc                 ó°   •— ‰r‰d   ‰ u r‰j                  d«       ‰s9‰j                  j                  ‰d«       ‰j                  j                  ‰d«       yy)zCPop the acknowledged head (retry lock held). True if queue drained.r   NTF)r^   rY   rj   )r}   r`   rl   r   r   s   €€€€€r   Ú	_ack_headzJSessionTranscriptMixin._append_to_transcript_serialized.<locals>._ack_headë   sR   ø€ á˜7 1™:¨Ñ,Ø—‘˜A”ÙØ×'Ñ'×+Ñ+Ð,<¸dÔCØ×0Ñ0×4Ñ4°ZÀÔFØØr   )ÚStateDbCorruptErrorÚStateDbReplacedError)ÚCompressionSessionClosedErrorr‚   zfSession DB transcript append rejected for compression-ended %s with no unique live child; not retryingé   z’Session DB transcript append failed for %s (failure_count=%d, pending=%d, spooled_to_disk=%d); session is stalled and needs operator attention: %szYSession DB transcript append failed for %s (failure_count=%d, pending=%d); will retry: %s©r   Úbool)rg   rb   Ú_drain_spooled_dropsÚ_append_transcript_messager!   rk   rŠ   r‹   Úhermes_state_errorsrŒ   Ú
isinstancerp   Ú_owner_key_for_session_idrx   rG   rR   r€   r†   r2   re   Ú_is_fts_corruption_errorÚ_rebuild_fts_oncerj   ri   Ú/_TRANSCRIPT_APPEND_FAILURE_ESCALATION_THRESHOLDÚ_spool_stalled_backlogr\   r_   )r   r   r"   r‰   Úqueue_emptyrm   rŠ   r‹   rŒ   Ú
_owner_keyr|   Úreroute_excÚ	retry_excÚfailuresÚspooledr}   r`   rl   s   ``             @@@r   rL   z7SessionTranscriptMixin._append_to_transcript_serializedß   sJ  ü€ ð ×(Ñ(ñ 	Ø×6Ñ6°zÀ7ÓKˆGØ˜!‘*ˆC÷	ð &Ð÷	ñ 	ð ð ×%Ñ% jÔ1ðGØ×/Ñ/°
¸CÔ@ð@ ×0Ñ0ñ )Ù"+£+�KÙ&Ø% a™j˜÷)ñ ØØ÷w	ð 	ú÷j)ð )ûô ó =ßRÝMÜ˜cÐ$8Ð:MÐ#NÔOØ×=Ñ=¸jÐJZÐ\_Ô`ÜÜ˜cÐ#@ÕAà!%×!?Ñ!?À
Ó!K�JØ#×;Ñ;¸JÓG�HÙñ &ØQ[˜DŸJ™JÐ'=¼tÓDÀXÑNð%Ø ×;Ñ;¸HÀcÔJð "&×!<Ñ!<ñ <Ø*.×*QÑ*QØ$.Ð0@À(ÈGÐUXó+Z à3;Ð 0÷<÷ <ñ <úð !×<Ñ<¸ZÈÔRÙ#*Ü &Ø")¨!¡*˜CØ)1˜JÝ$øô  )ò .Ø"-�Cûð.úð  "×8Ñ8ñ (Ù%œK÷(÷ (ñ (úäŸ™ðAØBLôNô Ø×0Ñ0°Ô5¸$×:PÑ:PÔ:Rð!Ø×7Ñ7¸
ÀCÔHð "×8Ñ8ñ (Ù%œK÷(÷ (ñ (úå øô %ò (Ø'�ûð(úð ×0Ñ0ñ LØ#×?Ñ?×CÑCÀJÐPQÓRÐUVÑV�HØCK�D×4Ñ4°ZÑ@÷L÷ Lñ Lúð ˜t×SÑSÒSØ"×9Ñ9¸*ÐFVÓW�GÜ—L‘Lð(à)3°X¼sÀ7»|ÈWÐVYõ[ô
 —N‘Nð)Ø*4°hÄÀGÃÈcôSô ûð{=ús÷   ’B
ÁB" Á/BÂ
BÂBÂ"
L6Â,/L1Ã AL1Ä-FÄ?L1ÅE,Å#	L1Å,E5	Å1L1ÆL1Æ	F3Æ'F.Æ)L1Æ.F3Æ3L1ÇGÇ
	L1ÇG	ÇL1Ç:!L1ÈIÈ.L1È:IÉ	L1ÉI	ÉL1É	I2É&I-É(L1É-I2É2L1Ê/J9Ê0	L1Ê9K	Ê>A.L1Ì1L6c                óª  — | j                   5  | j                  j                  |g «      }t        |«      }ddd«       d}D ]  }t	        ||«      € n|dz  }Œ |r_| j                  dt        «      j                  |«       | j                   5  d|…= |s| j                  j                  |d«       ddd«       |S |S # 1 sw Y   ŒˆxY w# 1 sw Y   |S xY w)au  Move a stalled session's in-memory backlog (oldest first) to the on-disk pending spool so
        a crash/restart during the outage no longer loses it (#114266): ``recover_pending_to_db``
        replays it at boot, ``_drain_spooled_drops`` before the next live write. Stops at the first
        spool failure so order holds; whatever stays in memory remains under the cap.Nr   r�   rX   )	rg   rY   ri   rh   r#   rG   rS   rT   r^   )r   r   rl   r`   Úbacklogrž   r"   s          r   r˜   z-SessionTranscriptMixin._spool_stalled_backlogC  sæ   € ð
 ×(Ñ(ñ 	$Ø×-Ñ-×1Ñ1Ð2BÀBÓGˆGÜ˜7“mˆG÷	$ð ˆØò 	ˆGÜ˜j¨'Ó2Ð:ÙØ�q‰L‰Gð	ñ Ø�J‰JÐ/´Ó5×9Ñ9¸*ÔEØ×,Ñ,ñ HØ˜H˜W˜HÐ%ÙØ×+Ñ+×/Ñ/Ð0@À$ÔG÷Hð ˆˆwˆ÷	$ð 	$ú÷Hð ˆús   �(B<Â$CÂ<CÃCc                ón  ‡ ‡— t        ‰ dd«      }|r‰|vry	 ddlm} ‰ j                  5  t	        ‰ j
                  j                  ‰«      «      }ddd«        |‰ˆ ˆfd„¬«      \  }}|s|j                  ‰«       yy# 1 sw Y   Œ0xY w# t        $ r!}t        j                  d‰|«       Y d}~yd}~ww xY w)z²Replay cap-dropped spooled transcript messages after DB recovery. Best-effort: replay
        failures keep the spool files for the next successful flush; nothing here may raise.rX   Nr   )Údrain_transcript_spoolc                ó(   •— ‰j                  ‰| «      S )N)r‘   )r"   r   r   s    €€r   ú<lambda>z=SessionTranscriptMixin._drain_spooled_drops.<locals>.<lambda>e  s   ø€ ¨D×,KÑ,KÈJÐX_Ó,`€ r   )Údb_known_failingz+Failed to drain transcript spool for %s: %s)r…   r    r¢   rg   r�   rj   ri   Údiscardr!   r2   r_   )r   r   Úspooled_sessionsr¢   Úknown_failingÚ	_replayedrn   rm   s   ``      r   r�   z+SessionTranscriptMixin._drain_spooled_dropsX  sÃ   ù€ ô # 4Ð)AÀ4ÓHÐÙ :Ð5EÑ#EØð	[ÝEð ×,Ñ,ñ WÜ $ T×%EÑ%E×%IÑ%IÈ*Ó%UÓ V�÷Wá#9ØÔ`Ø!.ô$Ñ ˆI�yñ Ø ×(Ñ(¨Õ4ð ÷Wð Wûô ò 	[Ü�N‰NÐHÈ*ÐVY×ZÑZûð	[ús.   ˜B
 ª%A>Á-B
 Á>BÂB
 Â
	B4ÂB/Â/B4c                ó‚  — | j                  |«      }|€t        d|› d�«      ‚|j                  d«      dk(  } |j                  d||j                  dd«      |j                  d«      |j                  d«      |j                  d	«      |j                  d
«      dœt        D �ci c]  }||r|j                  |«      nd“Œ c}¤|j                  d«      xs |j                  d«      t        |j                  d«      «      |j                  d«      t        |«      |j                  d«      |j                  d«      dœ¤Ž yc c}w )z7Write one transcript row. Caller handles retry queuing.Nzno owning session store for z; deferring transcript writeÚroleÚ	assistantÚunknownÚcontentÚ	tool_nameÚ
tool_callsÚtool_call_id)r   r«   r®   r¯   r°   r±   Úplatform_message_idÚ
message_idÚobservedÚ	timestampÚdisplay_kindÚdisplay_metadata)r²   r´   rµ   Úapi_contentr¶   r·   © )r0   ÚRuntimeErrorri   Úappend_messageÚ_ASSISTANT_ONLY_KEYSr�   r   )r   r   r"   Ú_dbÚis_assistantÚks         r   r‘   z1SessionTranscriptMixin._append_transcript_messagem  s1  € à×%Ñ% jÓ1ˆØˆ;ô Ø.¨z¨lÐ:VÐWóYð Yà—{‘{ 6Ó*¨kÑ9ˆØˆ×Ñð 	
Ø!Ø—‘˜V YÓ/Ø—K‘K 	Ó*Ø—k‘k +Ó.Ø—{‘{ <Ó0Ø Ÿ™ ^Ó4ñ	
ô EYÖY¸qˆq¡L�'—+‘+˜a”.°dÑ:ÒYð	
ð ")§¡Ð-BÓ!CÒ!`ÀwÇ{Á{ÐS_ÓG`Ü˜'Ÿ+™+ jÓ1Ó2Ø—k‘k +Ó.ô 4°GÓ<ð !Ÿ™ ^Ó4Ø$Ÿ[™[Ð);Ó<õ%	
ùò Zs   Â D<c                ó    — dt        | «      j                  «       v ryddl}ddlm} t        | |j                  «      xr |j                  | «      S )u™  True only when the failure is provably scoped to the FTS index. A bare SQLITE_CORRUPT
        can mean structural B-tree damage; only errors naming ``messages_fts`` or carrying FTS
        provenance (``SessionDB._is_fts_write_corruption_error``) may authorize the one-shot
        rebuild-and-retry; everything else takes the retry path.

        A generic ``database disk image is malformed`` (bare SQLITE_CORRUPT) can mean structural damage to
        canonical B-trees, not just the FTS shadow tables â€” treating it as FTS-only here made the store
        rebuild the index and retry transcript writes against a structurally corrupt database (#97940).
        Úmessages_ftsTr   N)Ú	SessionDB)r   ÚlowerÚsqlite3rk   rÂ   r“   ÚDatabaseErrorÚ_is_fts_write_corruption_error)rm   rÄ   rÂ   s      r   r•   z/SessionTranscriptMixin._is_fts_corruption_error‹  sD   € ð œS ›XŸ^™^Ó-Ñ-ØÛÝ*Ü˜#˜w×4Ñ4Ó5Òg¸)×:bÑ:bÐcfÓ:gÐgr   c                óÒ  — t        j                  «       }| j                  }|�||z
  | j                  k  ry| j                  }|�t        |d«      sy|| _        d}t        |d«      r|j                  «       }|rt        j                  d|«       y	 |j                  «       }|rt        j                  d|«       |dkD  S # t        $ r }t        j                  d|«       Y d}~yd}~ww xY w)	aG  Attempt FTS5 ``rebuild``, at most once per ``_FTS_REBUILD_COOLDOWN_SECONDS`` window;
        True if any index was rebuilt.

        A permanent one-shot flag meant a single rebuild failure (e.g. a transient WAL
        split-brain guard hit) permanently disabled recovery for the life of the process, even
        though later corruption on the same store could be fixable. Retrying on a cooldown lets
        the store try again after the underlying condition (e.g. a foreign holder) has likely
        cleared, without hammering a database that is corrupt on every write.
        NFÚrebuild_ftsÚ_foreign_state_db_holdersz�Skipping Session DB FTS rebuild while foreign processes hold the database or WAL sidecars (%s); canonical transcript writes remain available.z!Session DB FTS rebuild failed: %sz;Rebuilt %d Session DB FTS index(es) after append corruptionr   )ÚtimeÚ	monotonicÚ_fts_rebuild_last_attempt_atÚ_FTS_REBUILD_COOLDOWN_SECONDSr½   ÚhasattrrÉ   r2   r_   rÈ   r!   )r   ÚnowÚlast_attemptr5   Úforeign_holdersÚrebuiltrm   s          r   r–   z(SessionTranscriptMixin._rebuild_fts_onceœ  sè   € ô �n‰nÓˆØ×8Ñ8ˆØÐ#¨¨|Ñ);¸t×?aÑ?aÒ(aØØ�X‰XˆØˆ:œW R¨Ô7ØØ,/ˆÔ)àˆÜ�2Ð2Ô3Ø ×:Ñ:Ó<ˆOÙÜ�N‰NðSØTcôeð ð	Ø—n‘nÓ&ˆGñ Ü�N‰NÐXÐZaÔbØ˜‰{Ðøô ò 	Ü�N‰NÐ>ÀÔDÜûð	ús   ÂB= Â=	C&ÃC!Ã!C&c                ó¶   — | j                   5  | j                  j                  |d«       | j                  j                  |d«       ddd«       y# 1 sw Y   yxY w)zHDrop queued pending messages so a rewrite/rewind doesn't re-insert them.N)rg   rY   r^   rj   )r   r   s     r   Ú_clear_dirty_transcriptz.SessionTranscriptMixin._clear_dirty_transcriptÀ  sP   € à×(Ñ(ñ 	CØ×#Ñ#×'Ñ'¨
°DÔ9Ø×,Ñ,×0Ñ0°¸TÔB÷	C÷ 	Cñ 	Cús   �9AÁAc                óœ   — | j                  |«      }|sy	 |j                  ||«      S # t        $ r t        j	                  dd¬«       Y yw xY w)a  Whether a message with this platform_message_id is persisted (False without a DB).

        Thin wrapper over SessionDB.has_platform_message_id(). Returns False when no DB is available
        (in-memory sessions). Used by the gateway's transient-failure dedupe guard (#47237).
        Fz%has_platform_message_id lookup failedTr.   )r0   Úhas_platform_message_idr!   r2   r3   )r   r   r²   r5   s       r   rÖ   z.SessionTranscriptMixin.has_platform_message_idÆ  sV   € ð ×$Ñ$ ZÓ0ˆÙØð	Ø×-Ñ-¨jÐ:MÓNÐNøÜò 	Ü�L‰LÐ@È4ˆLÔPÙð	ús   –( ¨ AÁ
Ac                óÜ   — | j                  | j                  |«      «      }| j                  |«      }|sy	 |j                  |«      S # t        $ r t
        j                  d|d¬«       Y yw xY w)u·   Role of the newest live conversation row on the route ``load_transcript`` reads (``None``
        when empty, no DB, or the read fails â€” the boundary write would fail the same way).Nz$transcript tail lookup failed for %sTr.   )r6   rM   r0   Úlatest_conversation_roler!   r2   r3   r4   s      r   Útranscript_tail_rolez+SessionTranscriptMixin.transcript_tail_roleÕ  sr   € ð ×9Ñ9¸$×:OÑ:OÐPZÓ:[Ó\ˆ
Ø×$Ñ$ ZÓ0ˆÙØð	Ø×.Ñ.¨zÓ:Ð:øÜò 	Ü�L‰LÐ?ÀÐVZˆLÔ[Ùð	ús   ¶A Á!A+Á*A+c                ó.  — | j                  |«      }|sy| j                  «       5  	 |j                  ||||¬«       | j                  |«       	 ddd«       y# t        $ r(}t        j                  d|«       Y d}~ddd«       yd}~ww xY w# 1 sw Y   yxY w)u
  Replace a session's transcript (/retry, /compress). DESTRUCTIVE by default:
        ``active_only=False`` DELETEs every row incl. soft-archived compaction history (pass
        ``active_only=True`` for sessions that may carry archived rows). True when the write lands
        or there is no DB, False on failure â€” callers committing a destructive change on top
        (/compress repointing) must check it. ``reject_active_turn_lease`` is for user-initiated
        rewrites that do not own the cross-process turn lease.T)Úactive_onlyÚreject_active_turn_leasez&Failed to rewrite transcript in DB: %sNF)r0   rJ   Úreplace_messagesr!   r2   r3   rÔ   )r   r   ÚmessagesrÛ   rÜ   r5   Úes          r   Úrewrite_transcriptz)SessionTranscriptMixin.rewrite_transcriptâ  s¦   € ð ×$Ñ$ ZÓ0ˆÙØØ×,Ñ,Ó.ñ 	ðð ×#Ñ#Ø °kØ-Eð $ô Gð ×(Ñ(¨Ô4Ø÷)	ð 	øô  ò Ü—‘ÐEÀqÔIÛ÷%	ð 	ûð ú÷!	ð 	ús3   ¥B§A¼BÁ	BÁ BÁ6BÂBÂBÂBc                óz  — 	 | j                  |«      }| j                  |«      }|j                  |«      xs |}t        «       }|rZ||vrV|j	                  |«       |j                  ||«      ry|j                  |«      }|r|j                  |«      s	 y|d   }|r||vrŒVy# t        $ r}t        |«      |‚d}~ww xY w)a  Find this accepted input on the canonical live continuation and its ancestors.

        Content and unrelated writers cannot establish ownership. Query only existence;
        compaction archives can contain many megabytes that replay never needs to load.
        TÚparent_session_idFN)
rM   r0   r1   rS   rT   Úhas_gateway_input_ownerrt   Ú_is_compression_child_rowr!   r   )r   r   ÚownerÚcurrentr5   rV   Úrowrß   s           r   Úhas_input_ownerz&SessionTranscriptMixin.has_input_owner  sË   € ð	9Ø×+Ñ+¨JÓ7ˆGØ×(Ñ(¨Ó1ˆBØ×,Ñ,¨WÓ5Ò@¸ˆGÜ“5ˆDÙ˜g¨TÑ1Ø—‘˜Ô!Ø×-Ñ-¨g°uÔ=ØØ—n‘n WÓ-�Ù "×">Ñ">¸sÔ"CØàð Ð1Ñ2�ñ ˜g¨TÒ1ð øÜò 	9Ü% jÓ1°qÐ8ûð	9ús$   ‚A*B  Á-%B  ÂB  Â 	B:Â)B5Â5B:c                ó¤  — | j                  |«      sg S | j                  |«      }t        j                  t        «      5  | j                  |«      }|j                  |«      xs |}ddd«       	 | j                  |«      j                  |d¬«      S # 1 sw Y   Œ,xY w# t        $ r*}t        j                  d||d¬«       t        |«      |‚d}~ww xY w)u  Load all messages from a session's transcript (state.db is canonical). Reads follow the
        same routing writes use â€” the in-memory reroute map, then the durable compression tip â€”
        otherwise the transcript "vanishes" while every message sits under the child.NT)Úrepair_alternationzVTranscript read failed for session %s; refusing to treat the conversation as empty: %sr.   )
r0   rM   Ú
contextlibÚsuppressr!   r1   Úget_messages_as_conversationr2   re   r   )r   r   r5   rß   s       r   Úload_transcriptz&SessionTranscriptMixin.load_transcript  sà   € ð ×&Ñ& zÔ2ØˆIØ×*Ñ*¨:Ó6ˆ
Ü× Ñ ¤Ó+ñ 	Jà×(Ñ(¨Ó4ˆBØ×/Ñ/°
Ó;ÒI¸zˆJ÷	Jð
	9à×*Ñ*¨:Ó6×SÑSØ¨tð Tó 5ð 5÷	Jð 	Jûô ò 	9ô �L‰LðØ'¨°Tð ô ;ô & jÓ1°qÐ8ûð	9ús#   ¾'BÁ.!B ÂBÂ	CÂ%%C
Ã
C)Úrequire_retryable_compositec               óþ  — | j                  |«      }|syddlm} | j                  «       5  	 |j	                  |t        |d«       ||¬«      }| j                  |«       |j                  |j                  |j                  dœcddd«       S # |$ r(}t        j                  d|«       Y d}~ddd«       yd}~wt        $ r ‚ t        $ r(}t        j                  d|«       Y d}~ddd«       yd}~ww xY w# 1 sw Y   yxY w)	u%  Back up ``n`` user turns via soft-delete (``active=0``), mirroring CLI ``/undo [N]``.
        Returns ``{"rewound_count", "turns_undone", "target_text"}`` or ``None`` (no DB / no rewindable
        turn / persistence failure); ``n`` clamps to the oldest user turn. ``require_retryable_composite``
        is the gateway ``/retry`` guard: the selected turn must be a composite carrier whose live payload
        is losslessly replayable as text â€” that replay-policy ``ValueError`` propagates so /retry can
        explain why the carrier is unsafe.Nr   )ÚRewindTargetUnavailableErrorr�   )Úrequire_retryableÚrequire_compositezrewind_session: %sz!rewind_session: rewind failed: %s)Úrewound_countÚturns_undoneÚtarget_text)r0   Úhermes_state_rewindrñ   rJ   Úrewind_user_turnr{   r2   r3   Ú
ValueErrorr!   rÔ   rô   rõ   Ú	live_text)r   r   Únrï   r5   rñ   Úoutcomerß   s           r   Úrewind_sessionz%SessionTranscriptMixin.rewind_session2  s   € ð ×$Ñ$ ZÓ0ˆÙØÝDØ×,Ñ,Ó.ñ 	6ðØ×-Ñ-Ø¤ Q¨£ 
Ð>YØ&Að .ó C�ð ×(Ñ(¨Ô4Ø%,×%:Ñ%:ÈG×L`ÑL`Ø#*×#4Ñ#4ñ6÷	6ñ 	6øð
 0ò Ü—‘Ð1°1Ô5Û÷	6ð 	6ûô ò ØÜò Ü—‘Ð@À!ÔDÛ÷	6ð 	6ûðú÷	6ð 	6úsF   «C3­ BÁ4C3ÂC0ÂB3Â&C3Â3C0ÃC+ÃC3Ã+C0Ã0C3Ã3C<N)r   úOptional[str]r   rþ   )r9   z'SessionEntry'r:   rþ   r;   rþ   r   r�   )rA   r   rB   r   rC   r   r   zOptional[SessionEntry])F)r   r   r"   úDict[str, Any]rN   r�   r   r   )r   r   r   r   )r   r   r"   rÿ   r   rh   )r   r   rl   r   rm   r!   r   r   )
r   r   rl   r   r|   r   r`   rh   r   rh   )r   r   r|   r   r   r   )r   r   r"   rÿ   r   r   )r   r   rl   r   r   Úintr   )rm   r!   r   r�   rŽ   )r   r   r²   r   r   r�   )r   r   r   rþ   )FF)
r   r   rÞ   úList[Dict[str, Any]]rÛ   r�   rÜ   r�   r   r�   )r   r   rå   r   r   r�   )r   r   r   r  )r�   )r   r   rû   r   rï   r�   r   zOptional[Dict[str, Any]]) r   r   r   r   r]   rÍ   r—   r6   r<   rD   rJ   rO   rM   rb   rp   rx   r€   r†   rL   r˜   r�   r‘   Ústaticmethodr•   r–   rÔ   rÖ   rÙ   rà   rè   rî   rý   r¹   r   r   r*   r*   .   s™  „ ñTð  #Ðð %(Ð!ð 78Ð3ó
ðØ#ðØ:Gðà+ðà04óðØðØ58ðØMPðà	óò(Eô^óóð.ØðØ14ðØ;Dðà	óó4ðØðØ14ðØ@CðØNRðà	óó.PóbóHó*[ó*
ð< òhó ðhó "óHCóóð TYØ).ð Øð Ø)=ð ØLPð à"&ð à37ó óD9ó.9ð0 )*ð6ØRWñ6Øð6Ø"%ð6ØKOð6à	!ô6r   r*   )r   r   r"   rÿ   )r   Ú
__future__r   rë   ÚloggingrH   rÊ   Úagent.turn_contextr   Útypingr   r   r   r   r	   Úgateway.sessionr
   Ú	getLoggerr2   rº   r   r#   r¼   r*   r¹   r   r   ú<module>r	     sf   ðñ,õ #ã Û Û Û Ý :ß ;Õ ;áÝ,ð 
ˆ×	Ñ	Ð,Ó	-€ôM˜,ô MóðÐ ÷`6ò `6r   