+
    jHàjþë  ã                  ó  € R t ^ RIHt ^ RIt^ RIt^ RIt^ RIt^ RIHt ^ RI	H
t
HtHtHtHtHt ^ RIHtHtHtHtHt ]P,                  ! R4      tR]! RR	4       R
]! RR4       R]P0                  ! RR7       R2tRtRt] R] R] R] R2t] R] R] R] R2tRtR]! R4       R]! RR	4       R]! RR4       R2tR t R!] R"]  R#2t!R!] R$]! R%4       R&]! R%4       R']! R%4       R(]  R)2t"R/t#R* R+ lt$R,t% ! R- R.4      t&R# )0zäGateway-facing SessionDB persistence: routing index, peers, orphans, heartbeats, handoffs.

Mixin bound onto ``SessionDB`` via the MRO; built on its ``_read_ctx`` /
``_execute_write`` / ``_write_sql`` / ``_read_all`` primitives.)ÚannotationsN)ÚPath)ÚAnyÚDictÚListÚOptionalÚSetÚTuple)Ú_RECOVERABLE_END_REASONS_SQLÚ_RESET_CHILD_SQLÚ_RESET_END_REASONS_SQLÚ_sql_json_extractÚ_sql_session_last_activeÚhermes_stateaÙ  
                    WITH RECURSIVE compression_lineage(id) AS (
                        SELECT ?
                        UNION
                        SELECT parent.id
                        FROM compression_lineage lineage
                        JOIN sessions child ON child.id = lineage.id
                        JOIN sessions parent ON parent.id = child.parent_session_id
                        WHERE parent.end_reason = 'compression'
                          AND zchild.model_configz$._branched_fromz' IS NULL
                          AND z$._delegate_fromz, IS NULL
                          AND NOT (Úchild)Úazk)
                          AND COALESCE(child.source, '') != 'tool'
                    )
                aÉ  
                SELECT s.*,
                       COALESCE(sp.prompt, s.system_prompt)
                           AS _system_prompt_resolved,
                       (COALESCE(s.message_count, 0) > 0 OR EXISTS (
                           SELECT 1 FROM messages WHERE messages.session_id = s.id LIMIT 1
                       )) AS _has_messages
                FROM sessions s
                LEFT JOIN system_prompts sp ON sp.hash = s.system_prompt_hash
z(s.handoff_state = 'completed')z‰                WHERE s.session_key = ?
                  AND s.source = ?
                  AND (s.ended_at IS NULL OR s.end_reason IN (z)
                       OR a  )
                  AND NOT EXISTS (
                      SELECT 1 FROM sessions b
                      WHERE b.session_key = s.session_key
                        AND b.source = s.source
                        AND b.ended_at IS NOT NULL
                        AND b.end_reason IN (a$  )
                        AND b.ended_at
                            > COALESCE(s.last_activity_at, s.started_at)
                  )
                ORDER BY _has_messages DESC,
                         COALESCE(s.last_activity_at, s.started_at) DESC
                LIMIT 1
                aª                  WHERE s.source = ?
                  AND COALESCE(s.user_id, '') = COALESCE(?, '')
                  AND COALESCE(s.chat_id, '') = COALESCE(?, '')
                  AND COALESCE(s.chat_type, '') = COALESCE(?, '')
                  AND COALESCE(s.thread_id, '') = COALESCE(?, '')
                  AND (? IS NULL OR COALESCE(s.profile_name, ?) = ?)
                  AND (s.ended_at IS NULL OR s.end_reason IN (a  )
                  AND (COALESCE(s.message_count, 0) > 0 OR EXISTS (
                      SELECT 1 FROM messages WHERE messages.session_id = s.id LIMIT 1
                  ))
                  AND NOT EXISTS (
                      SELECT 1 FROM sessions b
                      WHERE b.source = s.source
                        AND COALESCE(b.user_id, '') = COALESCE(s.user_id, '')
                        AND COALESCE(b.chat_id, '') = COALESCE(s.chat_id, '')
                        AND COALESCE(b.chat_type, '') = COALESCE(s.chat_type, '')
                        AND COALESCE(b.thread_id, '') = COALESCE(s.thread_id, '')
                        AND (? IS NULL OR COALESCE(b.profile_name, ?) = ?)
                        AND b.ended_at IS NOT NULL
                        AND b.end_reason IN (z÷)
                        AND b.ended_at
                            > COALESCE(s.last_activity_at, s.started_at)
                  )
                ORDER BY COALESCE(s.last_activity_at, s.started_at) DESC
                LIMIT 1
                zpd.id, d.session_key, d.chat_id, d.chat_type, d.thread_id, d.user_id, d.origin_json, d.display_name, d.end_reasonz”
                SELECT o.id, o.source, o.user_id, o.started_at,
                       o.parent_session_id, o.profile_name,
                       Úoa˜   AS last_active,
                       (SELECT COUNT(*) FROM messages m
                         WHERE m.session_id = o.id) AS message_count
                FROM sessions o
                WHERE o.session_key IS NULL
                  AND EXISTS (SELECT 1 FROM messages m
                               WHERE m.session_id = o.id)
                  AND COALESCE(o.source, '') != 'tool'
                  AND zo.model_configz IS NULL
                  AND zC IS NULL
                ORDER BY o.started_at ASC
                z2AND (? IS NULL OR COALESCE(d.profile_name, ?) = ?)z 
                        SELECT zé
                        FROM sessions d
                        WHERE d.id = ?
                          AND d.session_key IS NOT NULL
                          AND COALESCE(d.source, '') = COALESCE(?, '')
                          z
                        ú, Úda¡   AS last_active
                        FROM sessions d
                        WHERE d.session_key IS NOT NULL
                          AND d.id != ?
                          AND COALESCE(d.source, '') = COALESCE(?, '')
                          AND (COALESCE(d.user_id, '') = ''
                               OR COALESCE(?, '') = ''
                               OR d.user_id = ?)
                          AND z/ BETWEEN ? AND ?
                          AND z < ?
                          zk
                        ORDER BY last_active DESC
                        LIMIT 2
                        c               ó   € V ^8„  d   QhRR/# )é   Úreturnzdict[str, set[str]]© )Úformats   "Ú9/home/ubuntu/.hermes/hermes-agent/hermes_state_gateway.pyÚ__annotate__r   œ   s   € ÷ ñ Ð%8ñ ó    c           
     ó
  € V P                  R\        4       Uu0 uF  q^ ,          kK  	  pp\         UUu/ uF6  q3V9   g   K  Y0P                  RV R24       Uu0 uF  q^,          kK  	  upbK8  	  upp# u upi u upi u uppi )aý  Live column sets of the lazily-created tables that exist (``{}`` when none do).

``apply_telegram_topic_migration`` runs only on explicit ``/topic`` opt-in, so a store
can hold supported v1/v2 tables without ``profile_name`` for its whole life; likewise the
delivery ledger adds ``delivery_obligations.adapter_profile`` only when a gateway opens
it. Identity settlement must gate its column SQL on the column actually being there
rather than on table existence, and must not force those migrations (#113757).
zGSELECT name FROM sqlite_master WHERE type='table' AND name IN (?, ?, ?)zPRAGMA table_info('z'))ÚexecuteÚ_OPTIONAL_TABLE_NAMES)ÚconnÚrowÚexistingÚtables   &   r   Ú_optional_table_columnsr$   œ   sš   € ð #'§,¡,ØQÜô#ó  ñ #˜3�A—�ñ #€Hð  õ
 +ôá*ˆE°xÑ.?ô 	Qˆ§,¡,Ð1DÀUÀGÈ2Ð/NÔ"OÓPÑ"O˜3�A—�Ñ"OÑPÒPÙ*òð ùò ùò Qùós"   ™A5µA?ÁA?ÁA:Á*A?Á:A?zFUPDATE sessions SET handoff_state = 'failed', handoff_error = ? WHERE c                  ó<  € ] tR t^±tRtR R ltRRRRRRRRR	RR
RRRRRRR/	R R lltRR/R R lltRR/R R lltRR/R R llt	R R lt
R R ltR R ltR R  ltR!RZ/R" R# lltR$RR%R/R& R' lltR(RR)R*/R+ R, lltRRRRRRRRR	R/R- R. lltR/R/R0 R1 lltR2 R3 ltR4 R5 ltR6 R7 ltR8 R9 ltR: R; lt]R< R= l4       tR>RR?RR@R/RA RB lltRC RD ltRE RF ltRG RH ltRI RJ ltRK RL ltRM RN lt RO RP lt!RQ RR lt"RS RT lt#RUR/RV RW llt$RX RY lt%RZt&R# )[ÚSessionGatewayMixinzLRouting index, session peers/orphans, hygiene streaks, heartbeats, handoffs.c               ó$   € V ^8„  d   QhRRRRRR/# )r   Úholderszlist[tuple[int, str]]Úmin_age_secondsÚfloatr   z	list[int]r   )r   s   "r   r   Ú SessionGatewayMixin.__annotate__´   s$   € ÷ 4ñ 4Ø,ð4ØBGð4ØLUñ4r   c          
     óP  € ^ RI Hp ^ RIHpHp \
        P                  P                  R4      '       d   Vf   . #  ^ RIH	p \        P                  ! 4       p. pT! T P                  T4       Fœ  p	 TP                  T	4      p
T
P                  RR7       Uu. uF  q»P                  NK  	  ppT! T
P!                  4       YzP#                  4       ,
          TT! T
P%                  4       4      TR7      '       g   KŠ   TP'                  T
4       Kž  	  . pT F/  p
 T
P)                  4        TP'                  T
P*                  4       K1  	  T'       g   . #  TP1                  TRR	7      w  rïT F  p
 T
P3                  4        K  	  T'       d    TP1                  TRR	7       T# T#   \         d    . u # i ; iu upi   \         d     EKX  i ; i  TP,                  \.        3 d     KÒ  i ; i  \         d    . p L’i ; i  TP,                  \.        3 d     K®  i ; i  \         d     T# i ; i)
z¿Terminate old PPID-1 Desktop ephemeral backends with no client.

Fails closed: anything whose parent, age, argv, or network connections
cannot be proved safe remains a repair-blocking holder.)Úpsutil)Ú_concrete_state_db_holder_pidsÚ"_is_inactive_orphan_desktop_holderÚlinux)Ú_is_ephemeral_port_zero_backendÚinet)Úkind)ÚppidÚage_secondsr)   Úephemeral_backendÚconnection_statusesg      ø?)Útimeout)r   r-   Úhermes_state_dbfiler.   r/   ÚsysÚplatformÚ
startswithÚhermes_cli.dashboard_procsr1   Ú	ExceptionÚtimeÚdb_pathÚProcessÚnet_connectionsÚstatusr4   Úcreate_timeÚcmdlineÚappendÚ	terminateÚpidÚErrorÚOSErrorÚ
wait_procsÚkill)Úselfr(   r)   r-   r.   r/   r1   ÚnowÚ
candidatesrH   Úprocessr    ÚstatusesÚ	signalledÚ_goneÚalives   &&$             r   Ú%_reap_inactive_orphan_desktop_holdersÚ9SessionGatewayMixin._reap_inactive_orphan_desktop_holders´   s  € õ 	(ßjÜ�|‰|×&Ñ& w×/Ò/°6²>ØˆIð	ÝRô �iŠi‹kˆØˆ
Ù1°$·,±,ÀÖHˆCð
Ø Ÿ.™.¨Ó-�Ø4;×4KÑ4KÐQWÐ4KÔ4XÓYÑ4X¨DŸKœKÑ4X�ÐYÙ9Ø Ÿ™›°S×;NÑ;NÓ;PÕ5PØ$3Ù&EÀgÇoÁoÓFWÓ&XØ(0÷	2ó 2ñ
 ð2ð ×Ñ˜gÖ&ñ Ið  "ˆ	Û!ˆGðØ×!Ñ!Ô#Ø× Ñ  §¡Ö-ñ "÷ ØˆIð	Ø!×,Ñ,¨ZÀÐ,ÓE‰LˆEó ˆGðØ—‘–ñ ÷
 ðØ×!Ñ! %°Ð!Ô5ð ÐˆyÐøôQ ô 	ØŠIð	üò Zøô ô Ûðûð —L‘L¤'Ð*ô Úðûô ô 	ØŠEð	ûð
 —L‘L¤'Ð*ô Úðûô
 ô ØØÐðús~   »F Á/%F4ÂF/Â(AF4Ä+GÅG% Å(G8ÆH ÆF,Æ+F,Æ/F4Æ4GÇGÇG"Ç!G"Ç%G5Ç4G5Ç8HÈHÈH%È$H%Úuser_idNÚsession_keyÚchat_idÚ	chat_typeÚ	thread_idÚdisplay_nameÚorigin_jsonÚinclude_compression_ancestorsFÚtransport_profilec               óH   € V ^8„  d   QhRRRRRRRRRRRRR	RR
RRRRRRRRR/# )r   Ú
session_idÚstrÚsourcerW   z
str | NonerX   rY   rZ   r[   r\   r]   r^   Úboolr_   r   ÚNoner   )r   s   "r   r   r+   ê   sv   € ÷ <!ñ <!Øð<!Ø*-ð<!Ø8Bð<!ØXbð<!àð<!à/9ð<!àMWð<!ànxð<!ð  ð<!ð HLð<!ð &ð	<!ð 26ñ	<!r   c       
        óÊ   a aaaaaaaaa	aaa€ S'       d	   S'       g   R# SSSSSSSS	S3	pV
oS'       d   S.VOM. VOSNoVVVVV	VV VVVVVV3R lpS P                  V4       R# )aÆ  Persist the gateway routing peer for an existing session row. ``display_name`` / ``origin_json``:
``None`` leaves the stored value untouched (consumers read routing data from state.db, not
sessions.json). ``include_compression_ancestors`` keeps a compression lineage on one routing peer
when an explicit resume moves its tip to another lane; per-turn refreshes update only the supplied
row. Self-healing: a missing target row (deferred ``create_session`` write, or crash between routing
publication and row creation) is INSERTed with full identity rather than no-opped, so a gateway row
is never first-created by the identity-less lazy writer (``update_token_counts``) and left
unroutable forever.

See #9006.
Nc                óD  <€ T P                  S'       d   \        MR  RS'       d   RMR 2S4       S'       d   R# V P                  RS34      P                  4       fC   V P                  RSS
S
SS	SSSSSSP                  4       S\        P                  ! 4       34       R# R# )Ú a„  
                   UPDATE sessions
                   SET session_key = ?, source = ?, user_id = ?, chat_id = ?,
                       chat_type = ?, thread_id = ?,
                       display_name = COALESCE(?, display_name),
                       origin_json = COALESCE(?, origin_json),
                       transport_profile = COALESCE(?, transport_profile)
                   z0WHERE id IN (SELECT id FROM compression_lineage)zWHERE id = ?Nz+SELECT 1 FROM sessions WHERE id = ? LIMIT 1aÈ  INSERT INTO sessions (
                               id, source, created_source, user_id, session_key, chat_id,
                               chat_type, thread_id, display_name, origin_json,
                               profile_name, transport_profile, started_at
                           )
                           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                           ON CONFLICT(id) DO UPDATE SET
                               session_key = COALESCE(sessions.session_key, excluded.session_key),
                               chat_id = COALESCE(sessions.chat_id, excluded.chat_id),
                               chat_type = COALESCE(sessions.chat_type, excluded.chat_type),
                               thread_id = COALESCE(sessions.thread_id, excluded.thread_id),
                               display_name = COALESCE(sessions.display_name, excluded.display_name),
                               origin_json = COALESCE(sessions.origin_json, excluded.origin_json),
                               transport_profile = COALESCE(sessions.transport_profile, excluded.transport_profile),
                               created_source = COALESCE(sessions.created_source, excluded.created_source))r   Ú_COMPRESSION_LINEAGE_CTEÚfetchoneÚ_own_profile_namer?   )r    Ú	ancestorsrY   rZ   r\   r]   Úquery_paramsrM   ra   rX   rc   r[   r_   rW   s   &€€€€€€€€€€€€€r   Ú_doÚ<SessionGatewayMixin.record_gateway_session_peer.<locals>._do  s¯   ø€ Ø�L‰Lß1:Õ-ÀÐCð D÷ KTÑFÐYgÐhðlð ô
÷ Ùà�|‰|ÐIÈJÈ=ÓY×bÑbÓdÒlØ—‘ðnð"   ¨°¸+ÀwÐPYÐ[dÐfrØ  $×"8Ñ"8Ó":Ð<MÌtÏyÊyË{ð\ö%ñ mr   ©Ú_execute_write)rM   ra   rc   rW   rX   rY   rZ   r[   r\   r]   r^   r_   Úidentityrn   rl   rm   s   ffdddddddd$d  @@r   Úrecord_gateway_session_peerÚ/SessionGatewayMixin.record_gateway_session_peerê   sm   ÿü€ ÷  §Ùà˜ ¨'°9¸iÈÐWbØðˆð 2ˆ	ß2;˜
Ð. XÑ.ÐAXÀ8ÐAXÈZÐAXˆ÷$	÷ $	ñ $	ðJ 	×Ñ˜CÖ r   Úscoperh   c               ó(   € V ^8„  d   QhRRRRRRRR/# )r   rX   rb   Ú
entry_jsonru   r   re   r   )r   s   "r   r   r+   (  s*   € ÷ 
ñ 
°cð 
Àsð 
ÐVYð 
Ðcgñ 
r   c          
     ó¦   € V'       d	   V'       g   R# ^ RI HpHp V P                  RV R2RVRV/V! W\        P                  ! 4       4      C4       R# )z�Upsert one gateway routing entry (session_key -> SessionEntry JSON); ``scope``
namespaces the index per sessions_dir so two stores never share routing state.N©ÚROUTE_RETIRED_SQLÚroute_retired_paramsz„INSERT INTO gateway_routing (scope, session_key, entry_json, updated_at)
               SELECT :scope, :key, :entry, :now WHERE NOT z¦
               ON CONFLICT(scope, session_key) DO UPDATE SET
                   entry_json = excluded.entry_json,
                   updated_at = excluded.updated_atru   Úkey)Ú hermes_state_mutation_retirementrz   r{   Ú
_write_sqlr?   )rM   rX   rw   ru   rz   r{   s   &&&$  r   Úsave_gateway_routing_entryÚ.SessionGatewayMixin.save_gateway_routing_entry(  sV   € ÷ §*Ùß\Ø�‰ð<Ø<MÐ;Nð O4ð7ð
 �e˜U KÐnÑ3GÈÔae×ajÒajÓalÓ3mÐnö	
r   c               ó$   € V ^8„  d   QhRRRRRR/# )r   Úentriesúdict[str, str]ru   rb   r   úset[str]r   )r   s   "r   r   r+   7  s#   € ÷ (ñ (°~ð (ÐQTð (Ð^fñ (r   c          
     ó
  aaa	€ ^ RI HoHp \        P                  ! 4       pVP	                  4        UUu. uF(  w  rVV'       g   K  V'       g   K  RSRV/V! WVV4      CNK*  	  uppo	VV	V3R lpV P                  V4      # u uppi )zÑAtomically replace the routing index for *scope* (keys absent from *entries*
are removed); other scopes untouched. Returns the keys skipped as retired routes, which
the sessions.json mirror must leave out too.ry   ru   r|   c                ó  <€ V P                  R S34       S'       d   V P                  RS,           S4       V P                  RS34       Uu0 uF  q^ ,          kK  	  ppS Uu0 uF  q3R,          kK  	  upV,
          # u upi u upi )z+DELETE FROM gateway_routing WHERE scope = ?zuINSERT INTO gateway_routing (scope, session_key, entry_json, updated_at) SELECT :scope, :key, :entry, :now WHERE NOT z7SELECT session_key FROM gateway_routing WHERE scope = ?r|   )r   Úexecutemany)r    ÚrÚkeptr!   rz   Úrowsru   s   &   €€€r   rn   Ú@SessionGatewayMixin.replace_gateway_routing_entries.<locals>._do>  sŠ   ø€ Ø�L‰LÐFÈÈÔQßØ× Ñ ðCØEVõWàôð #'§,¡,Ð/hÐkpÐjrÔ"sÓtÑ"s˜Q�a—D�DÑ"sˆDÐtÙ*.Ó/©$ 3˜—J�J©$Ñ/°$Õ6Ð6ùò uùÚ/s   ÁA=Á"B)r}   rz   r{   r?   Úitemsrq   )
rM   r‚   ru   r{   rN   ÚkÚvrn   rz   rŠ   s
   &&d     @@r   Úreplace_gateway_routing_entriesÚ3SessionGatewayMixin.replace_gateway_routing_entries7  sw   ú€ ÷ 	]Ü�iŠi‹kˆØZa×ZgÑZgÔZiÔuÑZiÑRVÐRSÔmnÔMÔstÔM�˜% ¨ÐMÑ-AÀ!ÈÓ-LÓMÑZiÒuˆ÷	7ð ×"Ñ" 3Ó'Ð'ùó vs   ´A?ÁA?ÁA?c               ó    € V ^8„  d   QhRRRR/# )r   ru   rb   r   rƒ   r   )r   s   "r   r   r+   I  s   € ÷ Añ A°Sð AÀ.ñ Ar   c               ór   € V P                  RV34      pV Uu/ uF  q3R,          VR,          bK  	  up# u upi )z>Load routing entries for *scope* as {session_key: entry_json}.zCSELECT session_key, entry_json FROM gateway_routing WHERE scope = ?rX   rw   )Ú	_read_all)rM   ru   rŠ   rˆ   s   &$  r   Úload_gateway_routing_entriesÚ0SessionGatewayMixin.load_gateway_routing_entriesI  s;   € à�~‰~ÐcÐfkÐemÓnˆÙ;?Ó@¹4°a�-Õ  ! L¥/Ò1¹4Ñ@Ð@ùÒ@s   ˜4c               ó    € V ^8„  d   QhRRRR/# )r   Úolder_than_daysr*   r   úlist[dict[str, Any]]r   )r   s   "r   r   r+   N  s   € ÷ ''ñ ''À5ð ''ÐMañ ''r   c               óî   € V^ 8  d   \        RV: R24      h\        P                  ! 4       \        V4      R,          ,
          pV P                  RV34      pV Uu. uF  p\	        V4      NK  	  up# u upi )uú  Keyed, still-open rows with no evidence of a single turn (no messages, tokens, tool/API calls,
activity, or title): leaked fixtures or chats routed but never answered. Safe to drop â€” the gateway
mints a fresh session on the next message. Needs its own selector because ``bulk prune``/``archive``
are pinned to ``ended_at IS NOT NULL``. ``pinned``/``archived`` = explicit keep intent.

That is exactly the shape of a leaked test fixture (#82770) â€” and also of a chat that was routed but
never answered.
z"older_than_days must be >= 0, got zX: a negative retention builds a future cutoff that matches every never-active keyed row.g     õ@a˜  
            SELECT s.id, s.session_key, s.source, s.chat_id,
                   s.chat_type, s.user_id, s.started_at
              FROM sessions s
             WHERE s.session_key IS NOT NULL
               AND s.ended_at IS NULL
               AND s.title IS NULL
               AND s.last_activity_at IS NULL
               AND COALESCE(s.message_count, 0) = 0
               AND COALESCE(s.tool_call_count, 0) = 0
               AND COALESCE(s.api_call_count, 0) = 0
               AND COALESCE(s.input_tokens, 0) = 0
               AND COALESCE(s.output_tokens, 0) = 0
               AND COALESCE(s.pinned, 0) = 0
               AND COALESCE(s.archived, 0) = 0
               AND s.started_at IS NOT NULL
               AND s.started_at < ?
               AND NOT EXISTS (
                       SELECT 1 FROM messages m WHERE m.session_id = s.id
                   )
             ORDER BY s.started_at
            )Ú
ValueErrorr?   r*   r“   Údict)rM   r—   ÚcutoffrŠ   rˆ   s   &$   r   Ú list_never_active_keyed_sessionsÚ4SessionGatewayMixin.list_never_active_keyed_sessionsN  s„   € ð ˜QÔÜØ4°_Ñ4Gð H^ð ^ó_ð _ô —’“¤ oÓ 6¸Õ @ÕAˆØ�~‰~ðð, ˆIó/
ˆñ2 "&Ó&¡˜A”�Q–¡Ñ&Ð&ùÒ&s   ÁA2c               ó    € V ^8„  d   QhRRRR/# ©r   ra   rb   r   úOptional[dict[str, Any]]r   )r   s   "r   r   r+   w  s   € ÷ 
ñ 
¸Cð 
ÐD\ñ 
r   c                ó$  € V P                  R4       F]  p \        P                  ! VR,          ;'       g    R4      p\        T\        4      '       g   KC  TP                  R4      T8X  g   K[  Tu # 	  R#   \        \        \
        3 d     K|  i ; i)zµThe routing entry (any scope) whose current owner is *session_id*, or None. The id lives
only inside ``entry_json``, so matching is done in Python; an archived/rotated row has none.z&SELECT entry_json FROM gateway_routingrw   ú{}ra   N)	r“   ÚjsonÚloadsrš   Ú	TypeErrorÚRecursionErrorÚ
isinstancer›   Úget)rM   ra   r!   Úentrys   &&  r   Ú!gateway_routing_entry_for_sessionÚ5SessionGatewayMixin.gateway_routing_entry_for_sessionw  s}   € ð —>‘>Ð"JÖKˆCðÜŸ
š
 3 |Õ#4×#<Ð#<¸Ó=�ô ˜%¤×&Ô&¨5¯9©9°\Ó+BÀjÖ+PØ’ñ Lñ øô	 ¤	¬>Ð:ô Úðús   –A5¶A5Á5BÂBc               ó    € V ^8„  d   QhRRRR/# )r   Úsession_idsr„   r   zlist[tuple[str, str, str]]r   )r   s   "r   r   r+   ƒ  s   € ÷ ñ ¸ð ÐF`ñ r   c                óŠ  € . pV'       g   V# V P                  R4       F„  p \        P                  ! VR,          ;'       g    R4      p\        T\        4      '       g   KC  TP                  R4      T9   g   K[  TP                  TR,          TR,          TR,          34       K†  	  V#   \        \        \
        3 d     K£  i ; i)z¬``(scope, session_key, session_id)`` of ``gateway_routing`` rows pointing at any of
*session_ids*; the target id lives only inside ``entry_json``, so matching is in Python.z:SELECT scope, session_key, entry_json FROM gateway_routingrw   r£   ra   ru   rX   )
r“   r¤   r¥   rš   r¦   r§   r¨   r›   r©   rF   )rM   r®   Úfoundr!   rª   s   &&   r   Ú_routing_entries_for_sessionsÚ1SessionGatewayMixin._routing_entries_for_sessionsƒ  s¨   € ð -/ˆßØˆLØ—>‘>Ð"^Ö_ˆCðÜŸ
š
 3 |Õ#4×#<Ð#<¸Ó=�ô ˜%¤×&Ô&¨5¯9©9°\Ó+BÀkÖ+QØ—‘˜c '�l¨C°Õ,>ÀÀlÕ@SÐTÖUñ `ð ˆøô	 ¤	¬>Ð:ô Úðús   ¢B(ÁB(Â(CÃCc               ó    € V ^8„  d   QhRRRR/# )r   r®   r„   r   Úintr   )r   s   "r   r   r+   ’  s   € ÷ ñ Àð ÈSñ r   c                ó²   € V P                  V4       UUUu. uF	  w  r#qBV3NK  	  ppppV'       g   ^ # V P                  RVRR7       \        V4      # u upppi )z?Drop ``gateway_routing`` rows pointing at any of *session_ids*.z?DELETE FROM gateway_routing WHERE scope = ? AND session_key = ?T)Úmany)r±   r~   Úlen)rM   r®   ru   r|   Ú_Údoomeds   &&    r   Ú$_delete_routing_entries_for_sessionsÚ8SessionGatewayMixin._delete_routing_entries_for_sessions’  sU   € à48×4VÑ4VÐWbÔ4cÕdÑ4c¡= 5¨q˜#“,Ñ4cˆÒdßÙØ�‰ÐYÐ[aÐhlˆÔmÜ�6‹{Ðùô	 es   –AÚexpiringc               ó   € V ^8„  d   QhRR/# )r   r   re   r   )r   s   "r   r   r+   š  s   € ÷ !ñ !Ètñ !r   c               ó¶   aaa€ \        V Uu0 uF  qD'       g   K  \        V4      kK  	  up4      oS'       g   R# VVV3R lpV P                  V4       R# u upi )a  Publish routing fences for *session_ids*, retired in the profile whose routing keys use
*namespace*, in THIS (routing) database and drop that profile's routes naming them, in one
transaction (``fence_shared_routes``). Routing saves skip a fenced route, so a whole-index
snapshot taken before a delete in another profile's store cannot restore it when it is
saved after it. The fence is not this store's own deletion marker: this profile's creates,
binds and generation walks stay free. *expiring* ids (a sweep's) age out at the horizon.Nc                ó,   <€ ^ RI Hp V! V SSSR7       R# )é    )Úfence_shared_routes)r¼   N)r}   rÁ   )r    rÁ   r¼   ÚidsÚ	namespaces   & €€€r   rn   Ú6SessionGatewayMixin.retire_gateway_routes.<locals>._do¥  s   ø€ ÝLÙ  i°¸x×Hr   )Úsortedrb   rq   )rM   r®   rÃ   r¼   Úsidrn   rÂ   s   &&dd  @r   Úretire_gateway_routesÚ)SessionGatewayMixin.retire_gateway_routesš  sG   ú€ ô ©+Ó=©+ 3»”h”c˜#–h©+Ñ=Ó>ˆßÙ÷	Ið 	×Ñ˜CÖ ùò >s
   �	A›AÚsessions_dirÚdeleted_idsc               ó(   € V ^8„  d   QhRRRRRRRR/# )	r   r—   r*   rÉ   zOptional[Path]rÊ   zOptional[list]r   ztuple[int, int, int]r   )r   s   "r   r   r+   ª  s,   € ÷ #8ñ #8Ø"'ð#8Ø7Eð#8à#ð#8à/Cñ#8r   c               ó’  a€ V P                  VR7      p^ RIHp V P                  4       ;_uu_ 4       pV Uu0 uFE  qu! V\	        VR,          4      4      \	        VR,          4      .8X  g   K3  \	        VR,          4      kKG  	  ppRRR4       X'       g   R# . p	V P                  V4      p
V P                  \        V4      VRV	R7      p\        V	4      oV P                  VS,
          4       \        V3R lV
 4       4      pVe"   VP                  \        VS,
          4      4       W¼\        V	4      3# u upi   + '       g   i     Lº; i)	uÀ  Delete never-active keyed rows and the routing entries naming them; returns
``(sessions_deleted, routing_entries_deleted, sessions_skipped)``. Deletion goes through
:meth:`delete_sessions` (one transaction; delegate cascade, FTS, transcripts).
Rows under a live turn lease or compression lock are skipped (#123583): the
never-active predicate reads committed state, so a keyed row whose first turn lease
is already held â€” its messages not yet flushed â€” would otherwise be deleted
mid-turn. Routing entries are dropped only after the rows commit, for every
candidate except the guarded ones: rows that vanished concurrently are gone too,
and a stale entry outliving its target would have the gateway resume a
nonexistent id. The row sweep is one transaction; routing entries are dropped in a
second write after it commits. ``sessions_skipped`` counts only the guarded rows.
``deleted_ids`` (the owning gateway) receives every candidate id that was not kept.)r—   )Úowned_lineage_idsÚidNT)rÉ   Úexclude_active_write_guardsÚskipped_idsc              3  ó@   <"  € T F  w   rVS9  g   K  ^x € K  	  R# 5i)é   Nr   )Ú.0r¸   rÆ   r‰   s   &  €r   Ú	<genexpr>ÚHSessionGatewayMixin.prune_never_active_keyed_sessions.<locals>.<genexpr>Ê  s   øé € ÐR±¡I A qÀ#ÈTÁ/Ÿaša³ùs   ƒ”
)rÀ   rÀ   rÀ   )r�   Úhermes_state_localrÍ   Ú	_read_ctxrb   r±   Údelete_sessionsÚlistÚsetrº   ÚsumÚextendrÅ   r·   )rM   r—   rÉ   rÊ   rO   rÍ   r    r!   rÂ   ÚskippedÚrouted_beforeÚdeletedÚrouting_deletedr‰   s   &$$$         @r   Ú!prune_never_active_keyed_sessionsÚ5SessionGatewayMixin.prune_never_active_keyed_sessionsª  s%  ø€ ð ×:Ñ:È?Ð:Ó[ˆ
õ 	9Ø�^‰^×Ô Ù-7Ów©Z cÐ;LÈTÔSVÐWZÐ[_ÕW`ÓSaÓ;bÔgjÐknÐosÕktÓguÐfvÑ;v”>”3�s˜4•y–>©ZˆCÐw÷ çØÐØˆð ×:Ñ:¸3Ó?ˆØ×&Ñ&Ü�‹I LÈdÐ`gð 'ó iˆä�7‹|ˆØ×1Ñ1°#¸µ*Ô=ÜÔR±ÓRÓRˆØÒ"Ø×Ñœv c¨D¥jÓ1Ô2Ø¬#¨g«,Ð7Ð7ùò x÷ ×ús"   ²D6·0D1Á,D1ÂD6Ä1D6Ä6E	r;   Úactive_onlyTc               ó$   € V ^8„  d   QhRRRRRR/# )r   r;   úOptional[str]rã   rd   r   r˜   r   )r   s   "r   r   r+   Ï  s)   € ÷ Rñ RØ(ðRØ>BðRØNbñRr   c               ó  € V P                  4        R\        R4       R2pV'       d   V.M. pY1'       d   RMRV'       d   RMR,           R,           ,          pV P                  W44       Uu. uF  qPP                  V4      NK  	  up# u upi )zŠList gateway sessions (rows with a session_key): newest row per key, one live
mapping per routing key. ``platform`` filters on ``source``.z¥
            SELECT sessions.*,
                   COALESCE(sp.prompt, sessions.system_prompt)
                       AS _system_prompt_resolved,
                   Úsessionsag   AS last_active
            FROM sessions
            LEFT JOIN system_prompts sp
              ON sp.hash = sessions.system_prompt_hash
            WHERE session_key IS NOT NULL
              AND started_at = (
                  SELECT MAX(s2.started_at) FROM sessions s2
                  WHERE s2.session_key = sessions.session_key
              )
        z AND LOWER(source) = LOWER(?)rh   z AND ended_at IS NULLz ORDER BY last_active DESC)Úflush_token_countsr   r“   Ú_session_row_dict)rM   r;   rã   ÚqueryÚparamsrˆ   s   &$$   r   Úlist_gateway_sessionsÚ)SessionGatewayMixin.list_gateway_sessionsÏ  s�   € ð
 	×ÑÔ!ðô -¨ZÓ8Ð9ð 	:	ð	ˆ÷ &.˜‘z°2ˆØ¶XÑ1À2ß'2Ñ#¸õ<Ø>Zõ[õ 	[ˆà37·>±>À%Ô3PÓQÑ3P¨a×&Ñ& qÖ)Ñ3PÑQÐQùÒQs   Á*Bc               ó4   € V ^8„  d   QhRRRRRRRRRRRRR	R
/# )r   rc   rb   rW   rå   rX   rY   rZ   r[   r   r¡   r   )r   s   "r   r   r+   è  sF   € ÷ *<ñ *<Øð*<Ø'4ð*<ØJWð*<àð*<à2?ð*<àS`ð*<ð 
"ñ*<r   c               óÖ  € V'       g   R# V P                  4       ;_uu_ 4       pVP                  \        W134      P                  4       pVe   V P	                  V4      uuRRR4       # Ve   Vf    RRR4       R# V P                  4       p	VP                  \        WWEWiW™W™V	34      P                  4       pRRR4       X'       d   V P	                  V4      # R#   + '       g   i     L+; i)up  Find the latest recoverable gateway session for a routing peer. The durable ``session_key`` on the row rebuilds a missing/pruned ``sessions.json`` mapping. Rows
ended only by the old ``agent_close`` bug or a mistaken TUI ``ws_orphan_reap`` are recoverable;
explicit boundaries (/new, /resume switches, compression splits) are not. Ranked by
``last_activity_at`` (fallback ``started_at`` â€” alone it resurrected days-old zombies); rows with
messages win, but an empty keyed row still beats ``None`` (which mints a new id while the transcript
may live under a compression child). Reset fence: a candidate is rejected when a peer boundary row
ended *after* its last activity, or the has-messages ranking could reach behind a /new. The
exact-key fallback requires the complete peer tuple (never cross chats/threads/users) plus a profile
fence: a Telegram DM's tuple is identical for every bot, so a sibling profile's legacy row would
otherwise be adopted. Ours = profile_name is the owner or NULL; stores outside the profile tree
derive no owner and stay unfenced.

See #60609.
Reset boundaries fence recovery (#68539): an intentional boundary such as ``session_reset`` (or any
explicit non-recoverable end_reason) must block fallback to an *older* row for the same peer. Each
candidate is therefore rejected when a boundary row for the peer ended *after* the candidate's last
activity â€” if the conversation's most recent event is an intentional reset, recovery returns nothing
rather than reaching behind it. The boundary row carries the same profile predicate as the
candidate: a sibling profile's reset must not suppress this profile's recovery.
N)r×   r   Ú_PEER_BY_KEY_SQLrj   ré   rk   Ú_PEER_BY_TUPLE_SQL)
rM   rc   rW   rX   rY   rZ   r[   r    r!   Úowners
   &$$$$$$   r   Ú$find_latest_gateway_session_for_peerÚ8SessionGatewayMixin.find_latest_gateway_session_for_peerè  sÏ   € ÷0 ÙØ�^‰^×Ô Ø—,‘,Ô/°+Ð1FÓG×PÑPÓRˆCØŠØ×-Ñ-¨cÓ2÷ Òð Š )Ò"3Ø÷ Ñð ×*Ñ*Ó,ˆEØ—,‘,Ü"Ø '°iÈÐV[ÐdiÐjó÷ ‰h‹jð ÷ ÷  /2ˆt×%Ñ% cÓ*Ð;°tÐ;÷! ×ús   £:CÁ(	CÁ;:CÃC(	Ú	max_gap_sc               ó    € V ^8„  d   QhRRRR/# )r   rõ   úOptional[float]r   r˜   r   )r   s   "r   r   r+     s   € ÷ 8ñ 8¸?ð 8ÐVjñ 8r   c               óî  a€ Vf   V P                   M
\        V4      pV P                  4       p. pV P                  4       ;_uu_ 4       pVP	                  \
        4      P                  4        EF|  pRpRpVR,          ;'       g    Tp	VR,          '       d@   Rp
VP	                  \        VR,          VR,          W3V	34      P                  4       pVf   RpM�Rp
VR	,          ;'       g    ^ pVP	                  \        VR
,          VR,          VR,          VR,          W²,
          W²,           VR,          W3V	3
4      P                  4       pV'       g	   RVR R2pM\        V4      ^8”  d   RpM	V^ ,          pTP                  RVR
,          RVR,          RVR,          R	VR	,          RVR,          RV'       d
   VR
,          MRRV'       d
   VR,          MRRV'       d   T
MRRVRJRV/
4       EK  	  RRR4       V Uau0 uF9  oSR,          '       g   K  \        V3R lV 4       4      ^8”  g   K0  SR,          kK;  	  ppV F  pVR,          V9   g   K  RVR&   RVR&   K  	  V#   + '       g   i     L{; iu upi )u,  Report message-bearing rows that lost their routing identity (messages, no ``session_key``).
Adoptable only when exactly one keyed predecessor can be named: ``lineage`` (``parent_session_id``
is a keyed row of the same source; no time window) or ``contiguity`` (exactly one keyed same-source
row with compatible ``user_id`` fell quiet within *max_gap_s* of the orphan's start and is older
than its last activity). Ambiguity is reported ``adoptable=False`` with a reason, never guessed â€”
mis-adopting splices one person's conversation into another's chat. The donor must share the
orphan's effective profile (NULL reads as this store's own): a sibling profile's keyed row in a
shared legacy store must never stamp its routing identity here. Branch/delegate/tool rows are
excluded: unkeyed by design, not damage.Nrh   Úprofile_nameÚparent_session_idÚlineagerc   z9parent session carries no gateway identity of this sourceÚ
contiguityÚ
started_atrÎ   rW   Úlast_activez'no keyed predecessor fell quiet within z.0fzs of this session's startz>ambiguous: more than one keyed predecessor matches this windowÚ	orphan_idÚmessage_countÚdonor_idrX   ÚevidenceÚ	adoptableÚreasonc              3  óT   <"  € T F  qR ,          SR ,          8X  g   K  ^x € K  	  R# 5i)r  Nr   )rÓ   Úxrˆ   s   & €r   rÔ   ÚESessionGatewayMixin.find_orphaned_gateway_sessions.<locals>.<genexpr>F  s    øé € Ð%[±¨A¸j½MÈQÈzÍ]Ñ<Z§a¢a³ùs   ƒ(ž
(Fz@ambiguous: more than one unkeyed session claims this predecessor)Ú_ORPHAN_ADOPTION_MAX_GAP_Sr*   rk   r×   r   Ú_ORPHANS_SQLÚfetchallÚ_ORPHAN_LINEAGE_DONOR_SQLrj   Ú_ORPHAN_CONTIGUITY_DONORS_SQLr·   rF   rÛ   )rM   rõ   Úgaprò   Úrecordsr    ÚorphanÚdonorr  Úorphan_ownerr  ÚstartedrO   rˆ   Ú	contestedÚrecords   &$           `  r   Úfind_orphaned_gateway_sessionsÚ2SessionGatewayMixin.find_orphaned_gateway_sessions  sq  ø€ ð 2;Ò1Bˆd×-Ò-ÌÈiÓHXˆØ×&Ñ&Ó(ˆØ(*ˆØ�^‰^×Ô ØŸ,™,¤|Ó4×=Ñ=×?�Ø�Ø�Ø% nÕ5×>Ð>¸�ØÐ-×.Ô.Ø(�HØ ŸL™LÜ1ØÐ 3Õ4°f¸XÕ6FÈÐVbÐcó÷ ‘h“jð ð ’}Ø!\˜øà+�HØ$ \Õ2×7Ð7°a�GØ!%§¡Ü5Ø � v¨hÕ'7¸À	Õ9JÈFÐS\ÕL]Ø �¨­°v¸mÕ7LØ |ð5ó"÷
 ‘h“jð ÷ &Ø#JÈ3ÈsÈ)ÐSlÐ!m™Ü˜Z›¨1Ô,Ø!a™à *¨1¥˜Ø—‘Ø ¨¥¨x¸ÀÕ9IØ# V¨OÕ%<¸lÈFÐS_ÕL`Ø! 6¨-Õ#8¸*×UZÀeÈDÆkÐ`dØ!¿5 5¨Ö#7ÀdØ¯E¡°r¸;ÈÐUYÐHYÐ[cÐekð m÷ nñ7 @÷ ñH $+ô
Ù#*˜aØ��~‰~ô Ü"%Ô%[±Ó%[Ó"[Ð^_Ñ"_ô ˆAˆj�MˆM¡7ð 	ð 
ó ˆFØ�jÕ! YÖ.Ø&+��{Ñ#Ø#e��xÓ ñ ð ˆ÷W ×üòF
sD   Á;IÂIÂAIÃ&CIÆ2IÇIÇIÇ9I2ÈI2È+I2ÉI/	c               ó$   € V ^8„  d   QhRRRRRR/# )r   rÿ   rb   r  r   rd   r   )r   s   "r   r   r+   N  s!   € ÷ 1(ñ 1(¸ð 1(Àsð 1(Ètñ 1(r   c                óŒ   aaa€ S'       d   S'       d   SS8X  d   R# V P                  4       oVVV3R lpV P                  V4      # )zÜStamp *orphan_id* with *donor_id*'s routing identity, retire *donor_id*.
Re-verifies the pair inside the write txn so a concurrent gateway that healed
either row makes this a no-op. Non-NULL orphan columns are preserved.Fc                ón  <€ V P                  R S34      P                  4       pV P                  RS34      P                  4       pVey   Veu   VR,          '       df   VR,          '       gW   VR,          ;'       g    RVR,          ;'       g    R8w  g.   Se,   VR,          ;'       g    SVR,          ;'       g    S8w  d   R# V P                  RVR,          VR,          VR	,          VR
,          VR,          VR,          VR,          SS3	4       V P                  R\        P                  ! 4       S34       R# )z†SELECT session_key, chat_id, chat_type, thread_id, user_id, origin_json, display_name, source, profile_name FROM sessions WHERE id = ?zCSELECT session_key, source, profile_name FROM sessions WHERE id = ?rX   rc   rh   rù   Fa6  UPDATE sessions
                      SET session_key = ?,
                          chat_id = COALESCE(chat_id, ?),
                          chat_type = COALESCE(chat_type, ?),
                          thread_id = COALESCE(thread_id, ?),
                          user_id = COALESCE(user_id, ?),
                          origin_json = COALESCE(origin_json, ?),
                          display_name = COALESCE(display_name, ?),
                          parent_session_id = COALESCE(parent_session_id, ?)
                    WHERE id = ? AND session_key IS NULLrY   rZ   r[   rW   r]   r\   zfUPDATE sessions SET ended_at = COALESCE(ended_at, ?), end_reason = 'superseded_by_repair' WHERE id = ?T)r   rj   r?   )r    r  r  r  rÿ   rò   s   &  €€€r   rn   Ú?SessionGatewayMixin.adopt_orphaned_gateway_session.<locals>._doU  s3  ø€ Ø—L‘Lð]à�ó÷ ‰h‹jð	 ð
 —\‘\ØUØ�óç&™h›jð ð ’ ¢°u¸]×7KÔ7KÈvÐVc×OdÔOdØ˜h�×-Ð-¨2°6¸(Õ3C×3IÐ3IÀrÔJØÒ)Ø" >Õ2×;Ð;°eÀÈÕAW×A`ÐA`Ð[`ÔaÙð �L‰Lð	<ð �}Õ% u¨YÕ'7¸¸{Õ9KÈUÐS^ÕM_Ø�yÕ! 5¨Õ#7¸¸~Õ9NÐPXÐZcðeôð  �L‰LðCä—’“˜hÐ'ô)ñ r   )rk   rq   )rM   rÿ   r  rn   rò   s   &ff @r   Úadopt_orphaned_gateway_sessionÚ2SessionGatewayMixin.adopt_orphaned_gateway_sessionN  s;   ú€ ÷ §¨I¸Ô,AÙØ×&Ñ&Ó(ˆ÷)	ðT ×"Ñ" 3Ó'Ð'r   c               ó    € V ^8„  d   QhRRRR/# )r   rX   rb   r   r´   r   )r   s   "r   r   r+   �  s   € ÷ (ñ (¸Cð (ÀCñ (r   c                óF   a€ S'       g   ^# V3R lpV P                  V4      # )zEAtomically increment the session-hygiene failure streak for one chat.c                ó�   <€ V P                  R S34       V P                  RS34      P                  4       p\        V^ ,          4      # )zëINSERT INTO gateway_hygiene_state (session_key, failure_streak)
                   VALUES (?, 1)
                   ON CONFLICT(session_key) DO UPDATE SET
                       failure_streak = gateway_hygiene_state.failure_streak + 1zFSELECT failure_streak FROM gateway_hygiene_state WHERE session_key = ?)r   rj   r´   )r    r!   rX   s   & €r   rn   ÚASessionGatewayMixin.increment_hygiene_failure_streak.<locals>._do…  sO   ø€ Ø�L‰LðTð �ôð —,‘,ØXÐ[fÐZhóç‰h‹jð ô �s˜1•v“;Ðr   rp   )rM   rX   rn   s   &f r   Ú increment_hygiene_failure_streakÚ4SessionGatewayMixin.increment_hygiene_failure_streak�  s!   ø€ çÙõ	ð ×"Ñ" 3Ó'Ð'r   c               ó    € V ^8„  d   QhRRRR/# )r   rX   rb   r   re   r   )r   s   "r   r   r+   “  s   € ÷ cñ c¸ð cÀñ cr   c                ó@   € V'       g   R# V P                  RV34       R# )z@Clear the persisted session-hygiene failure streak for one chat.Nz7DELETE FROM gateway_hygiene_state WHERE session_key = ?©r~   )rM   rX   s   &&r   Úreset_hygiene_failure_streakÚ0SessionGatewayMixin.reset_hygiene_failure_streak“  s   € çÙØ�‰ÐQÐT_ÐSaÖbr   c               ó$   € V ^8„  d   QhRRRRRR/# )r   Úold_namerb   Únew_namer   údict[str, int]r   )r   s   "r   r   r+   ™  s&   € ÷ kñ k¨Cð k¸3ð kÀ>ñ kr   c                ó  aaaaaa	€ T;'       g    RP                  4       T;'       g    RP                  4       uoo/ oS'       d   S'       d   SS8X  d   S# RS R2RS R2uo	o\        S	4      oVVVVVV	3R lpV P                  V4       S# )zAAtomically rewrite exact profile identity in this state database.rh   úagent:Ú:c           
     óŠ	  <€ V P                  R 4      P                  4        Uu0 uF  q^ ,          kK  	  pp\        V 4      pV P                  RSS^,           SS^,           SS34      P                  4       pVe"   \	        RV^ ,          : RV^,          : 24      hR* F|  w  rVRVP                  V\        4       4      9  d   K&  RP                  R V 4       4      pV P                  R	V R
V RV R2SS34      P                  4       pVf   Kp  \	        RV 24      h	  V P                  RSS34      P                  SR&   V P                  RSS34      P                  SR&   V P                  RSS^,           SS34      P                  SR&   ^ pV P                  R4      P                  4        F„  w  rš \        P                  ! V
4      p\        T\        4      '       g   K4  TP                  R4      S8X  g   KL  STR&   T P                  R\        P                  ! TRR7      T	34       T^,          pK†  	  VSR&   RV9   dk   RVP                  R\        4       4      9   d"   V P                  RSS34      P                  SR&   V P                  RSS^,           SS34      P                  SR&   R+ FL  pRVP                  V\        4       4      9   g   K$  V P                  R V R!2SS34      P                  SV R"2&   KN  	  R#VP                  R\        4       4      9   d+   V P                  R$SS^,           SS34      P                  SR%&   V P                  R&SS34      P                  4       pV EF  w  rÞpSVSR ,           pTpV'       dá    \        P                  ! V4      p\        V\        4      '       d´   \        VP                  R#4      \        4      '       d5   VR#,          P!                  S4      '       d   SVR#,          SR ,           VR#&   VP                  R'4      p\        V\        4      '       d   VP                  R4      S8X  d   SVR&   \        P                  ! VRR7      pV P                  R(VVV34       EK  	  \#        V4      SR)&   R# u upi   \        \        3 d     EKþ  i ; i  \        \        3 d    Rp EL'i ; i),ú1SELECT name FROM sqlite_master WHERE type='table'zñSELECT old.scope, ? || substr(old.session_key, ?) FROM gateway_routing AS old JOIN gateway_routing AS target ON target.scope = old.scope AND target.session_key = ? || substr(old.session_key, ?) WHERE substr(old.session_key, 1, ?) = ? LIMIT 1Nz#profile routing collision in scope z: Útelegram_dm_topic_bindingsrù   z AND c              3  ó2   "  € T F  pR V RV 2x € K  	  R# 5i)ztarget.z = old.Nr   )rÓ   Úcolumns   & r   rÔ   ÚGSessionGatewayMixin.rekey_profile_state.<locals>._do.<locals>.<genexpr>·  s!   é € ð (MÙDK¸&�g˜f˜X W¨V¨HÕ5ÃGùs   ‚zSELECT 1 FROM z AS old JOIN z* AS target ON target.profile_name = ? AND z# WHERE old.profile_name = ? LIMIT 1zprofile identity collision in z;UPDATE sessions SET profile_name = ? WHERE profile_name = ?Úsessions_profile_namez;UPDATE gateway_heartbeats SET profile = ? WHERE profile = ?Úgateway_heartbeats_profilezaUPDATE sessions SET session_key = ? || substr(session_key, ?) WHERE substr(session_key, 1, ?) = ?Úsessions_session_keyzBSELECT id, origin_json FROM sessions WHERE origin_json IS NOT NULLÚprofilez0UPDATE sessions SET origin_json = ? WHERE id = ?F)Úensure_asciiÚsessions_origin_jsonÚdelivery_obligationsÚadapter_profilezMUPDATE delivery_obligations SET adapter_profile = ? WHERE adapter_profile = ?Ú$delivery_obligations_adapter_profilezmUPDATE delivery_obligations SET session_key = ? || substr(session_key, ?) WHERE substr(session_key, 1, ?) = ?Ú delivery_obligations_session_keyzUPDATE z, SET profile_name = ? WHERE profile_name = ?Ú_profile_namerX   zsUPDATE telegram_dm_topic_bindings SET session_key = ? || substr(session_key, ?) WHERE substr(session_key, 1, ?) = ?Ú&telegram_dm_topic_bindings_session_keyz^SELECT rowid, session_key, entry_json FROM gateway_routing WHERE substr(session_key, 1, ?) = ?ÚoriginzJUPDATE gateway_routing SET session_key = ?, entry_json = ? WHERE rowid = ?Úgateway_routing))Útelegram_dm_topic_mode)rY   )r1  )rY   r[   )rC  r1  )r   r
  r$   rj   rš   r©   rÚ   ÚjoinÚrowcountr¤   r¥   r¦   r¨   r›   Údumpsrb   r<   r·   )r    r!   r"   Útopic_columnsÚ	collisionr#   ÚcolumnsÚequalityÚorigin_countra   r]   ÚpayloadÚroutingÚrowidrX   rw   Únew_session_keyÚnew_jsonrA  ÚcountsÚnewÚnew_nsÚns_lenÚoldÚold_nss   &                  €€€€€€r   rn   Ú4SessionGatewayMixin.rekey_profile_state.<locals>._do¢  s)  ø€ Ø*.¯,©,ØCó+EßEMÁXÃZð+Pó Qñ +P 3˜AŸ˜ñ +PˆHð Qä3°DÓ9ˆMØŸ™ðBð
 ˜ !� V¨V°a­Z¸ÀÐHó÷ ‰h‹jð ð Ò$Ü Ø9¸)ÀA½,Ñ9IÈÈIÐVWÍLÑK[Ð\ó^ð ^ó#‘�ð "¨×):Ñ):¸5Ä#Ã%Ó)HÔHÙØ"Ÿ<™<ñ (MÙDKó(Mó M�à ŸL™LØ$ U G¨=¸¸ð @6Ø6>°Zð @9ð9à;>À¸*óF÷ GOÁhÃjð ð Ô(Ü$Ð'EÀeÀWÐ%MÓNÐNñ#ð /3¯l©lØMÐPSÐUXÈzó/[ß[cÑ[cð Ð*Ñ+à37·<±<ØMÐPSÐUXÈzó4[ß[cÑ[cð Ð/Ñ0à-1¯\©\ð6à˜ !� V¨VÐ4ó.6÷ 7?±hð Ð)Ñ*ð
 ˆLØ+/¯<©<ØXó,ZßZbÑZbÓZdò,eÑ'�
ðÜ"Ÿjšj¨Ó5�Gô ˜g¤t×,Ô,°·±¸YÓ1GÈ3Ö1NØ),�G˜IÑ&Ø—L‘LÐ!SÜ"&§*¢*¨WÀ5Ô"IÈ:Ð!VôXà  AÕ%’Lñ,eð .:ˆFÐ)Ñ*à%¨Ô1Ø$¨×(9Ñ(9Ð:PÔRUÓRWÓ(XÔXØEIÇ\Á\ØgØ˜c˜
óF$ç$,¡Hð ÐAÑBð >B¿\¹\ð:à˜V a�Z¨°Ð8ó>:÷ ;C¹(ð Ð9Ñ:ó R�Ø! ]×%6Ñ%6°u¼c»eÓ%DÖDØ6:·l±lØ! % Ð(TÐUØ˜c˜
ó7$ç$,¡Hð ˜e˜W MÐ2Ó3ñ Rð
  × 1Ñ 1Ð2NÔPSÓPUÓ VÔVØCGÇ<Á<ð:ð ˜V a�Z¨°Ð8ó	D:÷ ;C¹(ð	 Ð?Ñ@ð —l‘lð6Ø8>ÀÐ7GóIçIQÉËð ô 3:Ñ.� JØ"(¨;°v°wÐ+?Õ"?�Ø%�ßð'Ü"&§*¢*¨ZÓ"8˜ô " '¬4×0Ò0Ü% g§k¡k°-Ó&@Ä#×FÒFÈ7ÐS`ÕKa×KlÑKlÐms×KtÒKtØ5;¸gÀmÕ>TÐU[ÐU\Ð>]Õ5]˜G MÑ2Ø!(§¡¨XÓ!6˜Ü% f¬d×3Ò3¸¿
¹
À9Ó8MÐQTÔ8TØ03˜F 9Ñ-Ü#'§:¢:¨gÀEÔ#J˜Ø—‘Ø`Ø$ h°Ð6÷8ñ 3:ô$ ),¨G«ˆFÐ$Ó%ùò}QøôV #¤IÐ.ô ÛðûôN '¬	Ð2ô 'Ø"&›ð'ús)   £RÆ/RÎR+ÒR(Ò'R(Ò+SÓS©Ústripr·   rq   )
rM   r)  r*  rn   rQ  rR  rS  rT  rU  rV  s
   &&& @@@@@@r   Úrekey_profile_stateÚ'SessionGatewayMixin.rekey_profile_state™  s‡   ý€ à—N�N ×)Ñ)Ó+¨h¯n¨n¸"×-CÑ-CÓ-EˆˆˆSØ!#ˆßŸ# ¨¤ØˆMØ! #  a˜¨F°3°%°q¨/ˆˆ�Ü�V“ˆ÷_	5ò _	5ðB 	×Ñ˜CÔ Øˆr   c               ó    € V ^8„  d   QhRRRR/# )r   r8  rb   r   r+  r   )r   s   "r   r   r+     s   € ÷ Fñ F¨3ð F°>ñ Fr   c                óº   aaaa€ T;'       g    RP                  4       o/ oS'       g   S# RS R2\        RS R24      uooVVVV3R lpV P                  V4       S# )u{  Delete exact profile identity from this state database (#111926, delete side).

The mirror of :meth:`rekey_profile_state`: a rename must rekey a profile's identity, a
delete must purge it. ``agent:<name>:*`` routing keys, ``gateway_heartbeats.profile``,
``delivery_obligations`` and the telegram topic tables are bookkeeping for a profile that
no longer exists â€” left behind, every inbound event on a chat keyed to the deleted name
enters the routing index, resolves a profile whose directory is gone, and logs
``Profile '<name>' does not exist`` on each event for the life of the store.

What each store gets, and why:

* Routing keys and heartbeat rows are hard-deleted â€” pure bookkeeping for a dead name.
* ``delivery_obligations`` rows are **terminalized** (``state='abandoned'``), not deleted:
  a pending obligation is delivery state that should not vanish silently, and the ledger's
  own retention prunes abandoned rows. Delivered history is left as it was.
* ``sessions`` rows are not deleted here: this helper settles identity, not history, and it
  does not decide what a delete leaves of a profile's conversation record â€” ``delete_profile``
  removes the profile's own home, ``state.db`` included, with the directory. Rows in a shared
  store keep whatever ownership they had; re-binding or archiving them belongs to the flow
  that recreates the name, not to this purge.

Idempotent.
rh   r-  r.  c                ó|  <€ V P                  R 4      P                  4        Uu0 uF  q^ ,          kK  	  pp\        V 4      pRV9   d"   V P                  RS
S	34      P                  SR&   RV9   d!   V P                  RS34      P                  SR&   RV9   d€   RVP	                  R\        4       4      9   d   RMRpV'       d   \        P                  ! 4       SS
S	3M\        P                  ! 4       S
S	3pV P                  R	V R
2V4      P                  SR&   RVP	                  R\        4       4      9   d!   V P                  RS34      P                  SR&   VP	                  R\        4       4      pRV9   dB   RV9   d   RMRpV'       d   SS
S	3MS
S	3pV P                  RV R2V4      P                  SR&   R# R# u upi )r0  rB  z?DELETE FROM gateway_routing WHERE substr(session_key, 1, ?) = ?Úgateway_heartbeatsz0DELETE FROM gateway_heartbeats WHERE profile = ?r;  r<  zadapter_profile = ? OR rh   zGUPDATE delivery_obligations SET state='abandoned', updated_at=? WHERE (zJsubstr(session_key, 1, ?) = ?) AND state NOT IN ('delivered', 'abandoned')rù   rC  z9DELETE FROM telegram_dm_topic_mode WHERE profile_name = ?r1  rX   zprofile_name = ? OR z-DELETE FROM telegram_dm_topic_bindings WHERE zsubstr(session_key, 1, ?) = ?N)r   r
  r$   rE  r©   rÚ   r?   )r    r!   r"   rG  Ú
by_profilerë   Úbinding_columnsrQ  ÚnameÚnsrT  s   &      €€€€r   rn   Ú4SessionGatewayMixin.purge_profile_state.<locals>._do$  s  ø€ Ø*.¯,©,ØCó+EßEMÁXÃZð+Pó Qñ +P 3˜AŸ˜ñ +PˆHð Qä3°DÓ9ˆMØ  HÔ,Ø,0¯L©LØUØ˜R�Ló-"ç"*¡(ð Ð(Ñ)ð $ xÔ/Ø/3¯|©|ØFÈÈó0QßQYÑQYð Ð+Ñ,à%¨Ô1ð "3°m×6GÑ6GÐH^Ô`cÓ`eÓ6fÔ!fñ 8à#%ð ÷ =Gœ$Ÿ)š)›+ t¨V°RÑ8ÌTÏYÊYË[ÐZ`ÐbdÐLe�Ø15·±ðØ(˜\ð *BðBàCIó2K÷ LTÉ8ð Ð-Ñ.ð  ×!2Ñ!2Ð3KÌSËUÓ!SÔSØ37·<±<ØOÐRVÐQXó4ZßZbÑZbð Ð/Ñ0à+×/Ñ/Ð0LÌcËeÓTˆOØ Ô/ð
 8FÈÔ7XÑ3Ð^`�
ß/9˜$ ¨Ñ+ÀÈ¸|�Ø7;·|±|ðØ'˜LÐ(EðGØHNó8PçPXÑPXð Ð3Ó4ñ 0ùò7Qs   £F9rX  )rM   r8  rn   rQ  rb  rc  rT  s   && @@@@r   Úpurge_profile_stateÚ'SessionGatewayMixin.purge_profile_state  sj   û€ ð0 —�˜2×$Ñ$Ó&ˆØ!#ˆßØˆMØ˜d˜V 1Ð%¤s¨V°D°6¸Ð+;Ó'<ˆ
ˆˆF÷%	Yð %	YðN 	×Ñ˜CÔ Øˆr   c               ó    € V ^8„  d   QhRRRR/# )r   Úsession_metar¡   r   zdict[str, Any]r   )r   s   "r   r   r+   O  s   € ÷ ,Cñ ,CÐ.Fð ,CÈ>ñ ,Cr   c                óÖ  € ^ RI Hp T ;'       g    / P                  R4      p\        V\        4      '       d    \
        P                  ! V4      p\        V\        4      '       g   / pVP                  R4      p\        V\        4      '       dB   VP                  R4      '       d+   VP                  4        UUu/ uF  w  rEVf   K  WEbK  	  upp# R
 Uu/ uF,  qbP                  V4      '       g   K  WbP                  V4      bK.  	  pp\	        T ;'       g    / P                  R4      ;'       g    R4      P                  4       pV'       d)   VP                  4       V9  d   VP                  RV4       M‡VP                  4       R8X  ds   VP                  R4      '       g\   \	        T ;'       g    / P                  R	4      ;'       g    R4      P                  4       p	V	'       d   VP                  RV	4       RVR&   V'       d   V# \        V\        4      '       g   / # VP                  4        UUu/ uF  w  rEVf   K  WEbK  	  upp#   \         d    / p ELi ; iu uppi u upi u uppi )u/  Read the persisted runtime route off a session row dict (``model_config`` as
JSON string or parsed dict). Precedence: nested ``gateway_runtime`` (gateway sync /
CLI ``/model``), then top-level ``provider``/``base_url``/``api_mode`` (TUI), with
``billing_provider`` filling a missing provider so sessions that never ran ``/model``
still restore the provider that served them; a bare ``custom`` billing class falls back
to its ``billing_base_url`` endpoint for the readers' bare-custom heal (#136323).
Empty dict on parse failure â€” resume uses ambient config.)Ú_BARE_BILLING_PROVIDERSÚmodel_configÚgateway_runtimeÚproviderÚbase_urlÚbilling_providerrh   ÚcustomÚbilling_base_url)rm  rn  Úapi_mode)r   rj  r©   r¨   rb   r¤   r¥   r>   r›   rŒ   rY  ÚlowerÚ
setdefault)
rh  rj  ÚrawÚruntimer�   rŽ   r|   Ú	top_levelro  rq  s
   &         r   Úsession_gateway_runtimeÚ+SessionGatewayMixin.session_gateway_runtimeN  sû  € õ 	9Ø×!Ð!˜r×&Ñ& ~Ó6ˆÜ�cœ3×ÒðÜ—j’j “o�ô ˜#œt×$Ò$ØˆCØ—'‘'Ð+Ó,ˆô �gœt×$Ò$¨¯©°Z×)@Ò)@Ø%,§]¡]¤_ÔF¡_™T˜Q¸”D�A’D¡_ÒFÐFÙ2VÓgÑ2V¨3×ZaÑZaÐbe×ZfÔ&�SŸ'™' #›,Ò&Ñ2Vˆ	Ðgô  × 2Ð 2°×7Ñ7Ð8JÓK×QÐQÈrÓR×XÑXÓZÐßÐ 0× 6Ñ 6Ó 8Ð@WÔ WØ× Ñ  Ð-=Õ>Ø×#Ñ#Ó%¨Ô1¸)¿-¹-È
×:SÒ:Sô  # L×$6Ð$6°B×#;Ñ#;Ð<NÓ#O×#UÐ#UÐSUÓV×\Ñ\Ó^ÐßØ×$Ñ$ ZÐ1AÔBØ(0�	˜*Ñ%ßØÐÜ˜'¤4×(Ò(ØˆIØ!(§¡¤ÔB¡™˜°A”�’¡ÒBÐBøô? ô Ø“ðüó GùÚgùó, Cs5   ¹I Â8IÃIÃI Ã0I È/I%È<I%ÉIÉIÚlast_heartbeatr8  Úhostc               ó4   € V ^8„  d   QhRRRRRRRRR	RR
RRR/# )r   Ú
backend_idrb   rH   r´   rý   r*   rz  r÷   r8  r{  r   re   r   )r   s   "r   r   r+   }  sM   € ÷ Yñ YØ ðYØ'*ðYØ8=ðYØO^ðYàðYà!$ðYà.2ñYr   c               óì   € V'       g   R# Vf   \         P                   ! 4       M
\        V4      pV P                  R\        V4      \	        V4      \        V4      V\        V4      \        V4      34       R# )a<  Upsert this backend's liveness row. ``backend_id`` MUST be stable for the process
lifetime (e.g. ``f"{profile}@{host}:{pid}"``) so a respawn cannot inherit a dead
predecessor's heartbeat; ``started_at`` is when THIS process started, so a backend
whose previous run died is not mistaken for a freshly-spawned sibling.Na,  INSERT INTO gateway_heartbeats (backend_id, pid, started_at, last_heartbeat, profile, host) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(backend_id) DO UPDATE SET pid = excluded.pid, started_at = excluded.started_at, last_heartbeat = excluded.last_heartbeat, profile = excluded.profile, host = excluded.host)r?   r*   r~   rb   r´   )rM   r}  rH   rý   rz  r8  r{  Útss   &$$$$$$ r   Úregister_backend_heartbeatÚ.SessionGatewayMixin.register_backend_heartbeat}  s\   € ÷ ÙØ*Ò2ŒT�YŠYŒ[¼¸nÓ8MˆØ�‰ð@ô �‹_œc #›h¬¨jÓ(9¸2¼sÀ7»|ÌSÐQUËYÐWö	Yr   c               ó    € V ^8„  d   QhRRRR/# )r   r}  rb   r   rd   r   )r   s   "r   r   r+   Ž  s   € ÷ [ñ [°#ð [¸$ñ [r   c                óT   € V'       g   R# V P                  R\        V4      34      ^ 8„  # )z‘Remove this backend's heartbeat row (from ``atexit``); True if removed. A crashed
backend's row is reclaimed later by ``prune_stale_heartbeats``.Fz3DELETE FROM gateway_heartbeats WHERE backend_id = ?)Ú_write_rowcountrb   )rM   r}  s   &&r   Úclear_backend_heartbeatÚ+SessionGatewayMixin.clear_backend_heartbeatŽ  s4   € ÷ ÙØ×#Ñ#ØAÄCÈ
ÃOÐCUóWØYZñ[ð 	[r   c               ó    € V ^8„  d   QhRRRR/# )r   Úmax_age_secondsr*   r   ú	list[str]r   )r   s   "r   r   r+   –  s   € ÷ 4ñ 4¸ð 4À9ñ 4r   c               ó¢   a€ V^ 8:  d   . # \         P                   ! 4       V,
          oV3R lp\        V P                  V4      ;'       g    . 4      # )u‡   Drop heartbeat rows older than the staleness window; return removed backend ids.
Safe from any process â€” only stale rows are touched.c                ó”   <€ V P                  R S34      pVP                  4        Uu. uF  p\        V^ ,          4      NK  	  up# u upi )zLDELETE FROM gateway_heartbeats WHERE last_heartbeat < ? RETURNING backend_id)r   r
  rb   )r    Úcurrˆ   rœ   s   &  €r   rn   Ú7SessionGatewayMixin.prune_stale_heartbeats.<locals>._doœ  sA   ø€ Ø—,‘,Ø^Ø�	óˆCð (+§|¡|¤~Ó6¡~ !”C˜˜!�–I¡~Ñ6Ð6ùÒ6s   §A)r?   rÙ   rq   )rM   rˆ  rn   rœ   s   &$ @r   Úprune_stale_heartbeatsÚ*SessionGatewayMixin.prune_stale_heartbeats–  sG   ø€ ð ˜aÔØˆIÜ—’“˜Õ.ˆõ	7ô
 �D×'Ñ'¨Ó,×2Ð2°Ó3Ð3r   c               ó   € V ^8„  d   QhRR/# ©r   r   r˜   r   )r   s   "r   r   r+   £  s   € ÷ 'ñ 'Ð)=ñ 'r   c                ód   € V P                  R4      pV Uu. uF  p\        V4      NK  	  up# u upi )zQSnapshot of every backend heartbeat (diagnostics/tests); fields mirror the table.zvSELECT backend_id, pid, started_at, last_heartbeat, profile, host FROM gateway_heartbeats ORDER BY last_heartbeat DESC)r“   r›   ©rM   rŠ   rˆ   s   &  r   Úlist_backend_heartbeatsÚ+SessionGatewayMixin.list_backend_heartbeats£  s3   € à�~‰~ð,ó-ˆñ "&Ó&¡˜A”�Q–¡Ñ&Ð&ùÒ&s   –-c               ó$   € V ^8„  d   QhRRRRRR/# )r   ra   rb   r;   r   rd   r   )r   s   "r   r   r+   ª  s!   € ÷ ñ ¨#ð ¸ð Àñ r   c                ó.   € V P                  RW!34      ^ 8„  # )zVMark a session pending handoff to *platform*; False if a handoff is already in flight.zÈUPDATE sessions SET handoff_state = 'pending',     handoff_platform = ?,     handoff_error = NULL WHERE id = ? AND (handoff_state IS NULL                   OR handoff_state IN ('completed', 'failed'))©r„  )rM   ra   r;   s   &&&r   Úrequest_handoffÚ#SessionGatewayMixin.request_handoffª  s-   € à×#Ñ#ðMð Ð"ó	
ð
 ñð 	r   c               ó    € V ^8„  d   QhRRRR/# r    r   )r   s   "r   r   r+   ³  s   € ÷ ñ ¨Cð Ð4Lñ r   c                óœ   €  V P                  RV34      pV'       g   R# RVR,          RVR,          RVR,          /#   \         d     R# i ; i)	zWReturn ``{"state", "platform", "error"}`` or None if the session has no handoff record.zPSELECT handoff_state, handoff_platform, handoff_error FROM sessions WHERE id = ?NÚstateÚhandoff_stater;   Úhandoff_platformÚerrorÚhandoff_error)Ú	_read_oner>   )rM   ra   r!   s   && r   Úget_handoff_stateÚ%SessionGatewayMixin.get_handoff_state³  s_   € ð		Ø—.‘.ØbØ�óˆC÷ ÙØ˜S Õ1°:¸sÐCUÕ?VØ˜S Õ1ð3ð 3øäô 	Úð	ús   ‚< Ÿ< ¼AÁ
Ac               ó   € V ^8„  d   QhRR/# r‘  r   )r   s   "r   r   r+   À  s   € ÷ 
ñ 
Ð';ñ 
r   c                ó–   €  V P                  R4      pV Uu. uF  q P                  V4      NK  	  up# u upi   \         d    . u # i ; i)zPAll sessions in handoff_state='pending', oldest first (gateway handoff watcher).zÕSELECT s.*, COALESCE(sp.prompt, s.system_prompt) AS _system_prompt_resolved FROM sessions s LEFT JOIN system_prompts sp ON sp.hash = s.system_prompt_hash WHERE s.handoff_state = 'pending' ORDER BY s.started_at ASC)r“   ré   r>   r“  s   &  r   Úlist_pending_handoffsÚ)SessionGatewayMixin.list_pending_handoffsÀ  sQ   € ð	Ø—>‘>ð,ó-ˆDñ
 8<Ó<±t°!×*Ñ*¨1Ö-±tÑ<Ð<ùÒ<øÜô 	ØŠIð	ús   ‚8 —3°8 ³8 ¸AÁAc               ó    € V ^8„  d   QhRRRR/# )r   ra   rb   r   rd   r   )r   s   "r   r   r+   Ì  s   € ÷ ñ ¨ð °ñ r   c                ó.   € V P                  RV34      ^ 8„  # )uC   Atomically transition pending â†’ running. Returns True if claimed.zXUPDATE sessions SET handoff_state = 'running' WHERE id = ? AND handoff_state = 'pending'r˜  ©rM   ra   s   &&r   Úclaim_handoffÚ!SessionGatewayMixin.claim_handoffÌ  s'   € à×#Ñ#ØfØˆMó
ð ñð 	r   c               ó   € V ^8„  d   QhRR/# )r   r   rd   r   )r   s   "r   r   r+   Ó  s   € ÷ lñ l dñ lr   c                ó(   € V P                  R4      RJ# )z[Bounded existence probe for the handoff watcher's idle gate (the gate fails open on error).z>SELECT 1 FROM sessions WHERE handoff_state = 'pending' LIMIT 1N)r¢  )rM   s   &r   Úhas_pending_handoffsÚ(SessionGatewayMixin.has_pending_handoffsÓ  s   € à�~‰~Ð^Ó_ÐgkÐkÐkr   c               ó    € V ^8„  d   QhRRRR/# )r   ra   rb   r   re   r   )r   s   "r   r   r+   ×  s   € ÷ ñ ¨3ð °4ñ r   c                ó,   € V P                  RV34       R# )zMark a handoff as completed.zRUPDATE sessions SET handoff_state = 'completed', handoff_error = NULL WHERE id = ?Nr%  r«  s   &&r   Úcomplete_handoffÚ$SessionGatewayMixin.complete_handoff×  s   € à�‰Ø`ØˆMö	r   Úonly_statesc               ó(   € V ^8„  d   QhRRRRRRRR/# )r   ra   rb   r   r¶  zOptional[tuple[str, ...]]r   rd   r   )r   s   "r   r   r+   Ý  s2   € ÷ Qñ QØðQØ&)ðQØ;TðQØ`dñQr   c               óÜ   € V'       d   \        V4      MRp\        R,           V'       d   RRP                  R V 4       4       R2MR,           pV P                  WRR,          V.VO54      ^ 8„  # )	ur  Mark a handoff failed and record the reason; True when a row transitioned. ``only_states`` makes
the write a compare-and-swap on ``handoff_state``. Waiters that give up (CLI 60s poll, Desktop
bounded poll) MUST pass ``only_states=("pending",)``: once the watcher has claimed the row
(``running``) it owns the terminal state, and an unconditional waiter-side fail races the dispatch â€”
the gateway later overwrites ``failed`` â†’ ``completed`` after the user was told the gateway is down
(split-brain: the handoff delivered and ``switch_session`` re-pointed the session). The watcher
fails its OWN claimed row unconditionally.zid = ?z AND handoff_state IN (r   c              3  ó&   "  € T F  pR x € K	  	  R# 5i)Ú?Nr   )rÓ   r¸   s   & r   rÔ   Ú3SessionGatewayMixin.fail_handoff.<locals>.<genexpr>è  s   é € Ð/D¹V¸µ»Vùs   ‚Ú)rh   ºNiô  Nr   )ÚtupleÚ_HANDOFF_FAIL_SQLrD  r„  )rM   ra   r   r¶  ÚstatesÚsqls   &&&$  r   Úfail_handoffÚ SessionGatewayMixin.fail_handoffÝ  sh   € ÷ (3”�{Ô#¸ˆÜ (Õ*ßKQÐ% d§i¡iÑ/D¹VÓ/DÓ&DÐ%EÀQÑGÐWYõ[ˆà×#Ñ# C°­+°zÐ)KÀFÑ)KÓLÈqÑPÐPr   c               ó    € V ^8„  d   QhRRRR/# )r   r   rb   r   r‰  r   )r   s   "r   r   r+   ë  s   € ÷ ñ °Cð ¸Iñ r   c                óœ   a€ V3R lp V P                  V4      ;'       g    . #   \         d    \        P                  RRR7       . u # i ; i)un  Fail every handoff stuck in ``running``; returns the ids reclaimed. Only the gateway watcher sets
``running``, for one in-process dispatch â€” so a ``running`` row at watcher startup belongs to a
PREVIOUS gateway that died mid-dispatch. It is poisonous: ``request_handoff`` only accepts
NULL/``completed``/``failed``, so the session could never hand off again, with no error surfaced.
Failing rather than re-queueing is deliberate: the dead gateway may already have switched the
session key and dispatched the synthetic turn, so a blind retry risks double delivery; a clean
terminal state the user can retry from is right.c                óØ   <€ V P                  R 4      pVP                  4        Uu. uF  q"^ ,          NK  	  ppV'       d&   V P                  \        R,           SR,          34       V# u upi )z7SELECT id FROM sessions WHERE handoff_state = 'running'zhandoff_state = 'running'r½  )r   r
  r¿  )r    rŒ  rˆ   rÂ   r   s   &   €r   rn   Ú?SessionGatewayMixin.reclaim_stale_running_handoffs.<locals>._doó  sY   ø€ Ø—,‘,ÐXÓYˆCØ!$§¡¤Ó0¡˜A�Q—4�4¡ˆCÐ0ßØ—‘Ô.Ð1LÕLÈuÐUYÍ{ÈnÔ]ØˆJùò 1s   ¥A'zbreclaim_stale_running_handoffs failed; stranded 'running' handoff rows (if any) were left in placeT)Úexc_info)rq   r>   ÚloggerÚwarning)rM   r   rn   s   &f r   Úreclaim_stale_running_handoffsÚ2SessionGatewayMixin.reclaim_stale_running_handoffsë  sX   ø€ õ	ð	Ø×&Ñ& sÓ+×1Ð1¨rÐ1øÜô 	ô �N‰Nð;ØEIð ô Kð ŠIð	ús   ‰$ ¡$ ¤$AÁ
Ar   )'Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__Ú__doc__rU   rs   r   r�   r”   r�   r«   r±   rº   rÇ   rá   rì   ró   r  r  r!  r&  rZ  re  Ústaticmethodrx  r€  r…  rŽ  r”  r™  r£  r§  r¬  r°  r´  rÂ  rË  Ú__static_attributes__r   r   r   r&   r&   ±   s²  † ÙVõ4ðl<!ØEIð<!Øeið<!à"ð<!à<@ð<!àZ^ð<!à{ð<!ð #'ð<!ð OTð<!ð )-÷	<!ð|
Ð\^÷ 
ð(ÐWY÷ (ð$A¸2÷ Aõ
''õR
õõð!È÷ !ð #8ØHLð#8à&*÷#8ðJRØ+/ðRØEI÷Rð2*<Ø7;ð*<ØZ^ð*<à!%ð*<àBFð*<àcg÷*<ðX8ÈT÷ 8õt1(õf(õ$cõkõZFðP ô,Có ð,Cð\YØaeðYàðYà')÷Yõ"[õ4õ'õõõ
õõlõðQØW[÷Q÷ñ r   r&   )rC  r1  r;  )'rÑ  Ú
__future__r   r¤   Úloggingr:   r?   Úpathlibr   Útypingr   r   r   r   r   r	   Úhermes_state_commonr
   r   r   r   r   Ú	getLoggerrÉ  r   ri   Ú_PEER_SELECT_HEADÚ_HANDOFF_OWNED_ROW_SQLrð   rñ   Ú_ORPHAN_DONOR_COLUMNSr	  Ú_ORPHAN_DONOR_PROFILE_FENCE_SQLr  r  r   r$   r¿  r&   r   r   r   Ú<module>rÞ     s  ðñCõ
 #ã Û Û 
Û Ý ß 8× 8÷õ ð
 
×	Ò	˜>Ó	*€ð
	 ñ  1Ð1EÐGYÓZÐ[ð \Ù0Ð1EÐGYÓZÐ[ð \$Ø$4×$;Ò$;¸gÔ$FÐ#Gð HðÐ ð"	Ð ð" ;Ð Ø)Ð*ð +?à?[Ð>\ð ]Ø1Ð2ð 3.ð /EÐ-Eð FðÐ ð" ,Ð,ð -?ð @\Ð>\ð ]Ø1Ð2ð 3.ð /EÐ-Eð Fð)Ð ð:=ð ðñ 1°Ó5Ð6ð 7ñ )Ð)9Ð;MÓNÐOð PÙ(Ð)9Ð;MÓNÐOð Pð€ð& #WÐ ð! Ø 5Ð6ð 7ð
 ;Ð;ð <ðÐ ð% Ø 5Ð6°bÑ9QÐRUÓ9VÐ8Wð Xñ  8¸Ó<Ð=ð >Ù7¸Ó<Ð=ð >Ø:Ð;ð <ð!Ð ðTÐ õð$ ]Ð ÷Pó Pr   