Ë
    âß²j©L  ã                  ó´   — d Z ddlmZ ddlZddlmZmZmZmZm	Z	m
Z
mZ ddlmZmZ  ej                   e«      Zdd„Zdd„Zdd„Z ed	d
h«      Z G d„ d«      Zy)u§  Crossed-profile durable state in ONE ``state.db`` â€” finders and fixers for
``hermes sessions repair-profiles`` (#88715 PR-6).

Every ``state.db`` belongs to exactly one profile (``<root>/state.db`` â†’ ``default``,
``<root>/profiles/<name>/state.db`` â†’ ``name``) and every gateway session key encodes the profile
that owns the conversation (``agent:main:â€¦`` for the default, ``agent:<name>:â€¦`` for a named one).
The per-profile store model (#88734) is forward-only: it routes NEW writes to the right store but
never touched rows that had already landed in the wrong one, and the identity fences
(``_INHERIT_PARENT_META_SQL``, ``_recovered_row_allowed_for_active_profile``) only refuse to
*widen* damage that already exists. This module is the backward-looking half: it names each crossed
row and can settle it.

Store-level only. Which profiles exist, which store owns which home, whether a live gateway holds the
routing index in memory, and the JSON files outside ``state.db`` are the orchestrator's concern
(``hermes_cli/sessions_repair_profiles.py``).

Moving a session between two stores is two single-store transactions â€” copy into the target, then
delete from the source â€” because a crash between them leaves a *duplicate*, which the next run
settles idempotently (:meth:`import_moved_session` reports ``present``; the delete only proceeds when
the target holds at least as many messages). The reverse order would lose the row.
é    )ÚannotationsN)ÚAnyÚDictÚIterableÚListÚOptionalÚSetÚTuple)Ú
_id_chunksÚ_placeholdersc                ó    — t        | t        «      sy| j                  d«      }t        |«      dk  s|d   dk7  s|d   syddlm}  ||d   «      S )u½   Profile encoded in an ``agent:<ns>:â€¦`` gateway key (``default`` for ``main``), or None for a
    keyless row (CLI/subagent lineage) or a key another producer minted (hosted rooms, tests).Nú:é   r   Úagenté   )Ú"profile_from_session_key_namespace)Ú
isinstanceÚstrÚsplitÚlenÚgateway.sessionr   )Úsession_keyÚpartsr   s      ú@/home/ubuntu/.hermes/hermes-agent/hermes_state_profile_repair.pyÚsession_key_profiler       sR   € ô �k¤3Ô'ØØ×Ñ˜cÓ"€EÜ
ˆ5ƒz�A‚~˜˜q™ WÒ,°E¸!²HØÝBÙ-¨e°A©hÓ7Ð7ó    c                óH   — t        | xs d«      j                  «       }|xs dS )zY``sessions.profile_name`` as a comparable label; NULL/blank is "unowned", not a crossing.Ú N)r   Ústrip)ÚvalueÚnames     r   Ú_stored_profiler"   ,   s#   € äˆuŠ{˜Ó×!Ñ!Ó#€DØŠ<�4Ðr   c                óX   — | j                  d|› d�«      D �cg c]  }|d   ‘Œ	 c}S c c}w )NzPRAGMA table_info('z')r   )Úexecute)ÚconnÚtableÚrows      r   Ú_table_columnsr(   2   s-   € Ø"Ÿl™lÐ-@ÀÀÀrÐ+JÓKÖL�sˆC�‹FÒLÐLùÒLs   ˜'ÚidÚdisplay_orderc                  ó’   — e Zd Z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dd„Zdd„Zdd„Zy)ÚSessionProfileRepairMixinz^Per-store crossed-profile identity: find, relabel, sever, move, and settle routing/topic rows.c           
     ó   — | j                  d«      }g g g dœ}|D ]±  }t        |d   «      }|€Œt        |d   «      }|�$||k7  r|d   j                  |d   |d   ||dœ«       ||k7  r/|d   j                  |d   |d   |t	        |d	   xs d
«      dœ«       t        |d   «      }|€Œ�||k7  sŒ“|d   j                  |d   ||d   |dœ«       Œ³ |S )uŽ  Keyed session rows whose identity disagrees with itself or with this store.

        ``mislabelled``: ``profile_name`` names a profile other than the one in the row's own key
        (the key is what routing, the agent cache and ``_db_for_key`` consult; the label is stamped
        after the fact). ``foreign``: the key's profile is not *owner* â€” the row sits in a store that
        the routed profile never reads. ``crossed_parents``: child and parent are both keyed and name
        different profiles, so the NULL-fill inheritance fence would have been the only thing standing
        between them. A row can appear in more than one list.
        zþSELECT s.id, s.session_key, s.profile_name, s.parent_session_id, s.message_count,        p.session_key AS parent_session_key FROM sessions s LEFT JOIN sessions p ON p.id = s.parent_session_id WHERE s.session_key LIKE 'agent:%' ORDER BY s.started_at, s.id)ÚmislabelledÚforeignÚcrossed_parentsr   Úprofile_namer.   r)   )r)   r   r1   Úkey_profiler/   Úmessage_countr   )r)   r   r2   r3   Úparent_session_keyr0   Úparent_session_id)r)   r2   r5   Úparent_profile)Ú	_read_allr   r"   ÚappendÚint)ÚselfÚownerÚrowsÚfoundr'   r2   Úlabelr6   s           r   Úfind_crossed_profile_sessionsz7SessionProfileRepairMixin.find_crossed_profile_sessionsB   s:  € ð �~‰~ðMóNˆð
 BDÐPRÐgiÑ1jˆØò 	fˆCÜ-¨c°-Ñ.@ÓAˆKØÐ"ØÜ# C¨Ñ$7Ó8ˆEØÐ  U¨kÒ%9Ø�mÑ$×+Ñ+Ø˜d™)°C¸Ñ4FØ$)¸+ñ-Gô Hð ˜eÒ#Ø�iÑ ×'Ñ'Ø˜d™)°C¸Ñ4FÐWbÜ%(¨¨_Ñ)=Ò)BÀÓ%Cñ)Eô Fô 1°Ð5IÑ1JÓKˆNØÑ)¨nÀÓ.KØÐ'Ñ(×/Ñ/Ø˜d™)°KØ),Ð-@Ñ)AÐUcñ1eõ fð	fð$ ˆr   c                ó\   — | j                  d«      D �cg c]  }t        |«      ‘Œ c}S c c}w )NzbSELECT scope, session_key, entry_json, updated_at FROM gateway_routing ORDER BY scope, session_key)r7   Údict)r:   r'   s     r   Úlist_gateway_routing_rowsz3SessionProfileRepairMixin.list_gateway_routing_rowsf   s0   € Ø%)§^¡^ð*ó&+ö ,˜c”�S•	ò ,ð 	,ùò ,s   ”)c                ó�   — d„ }g }| j                  |«      D ],  }t        |d   «      }|dvsŒ|j                  i |¥d|i¥«       Œ. |S )a  ``telegram_dm_topic_bindings`` rows labelled ``default`` whose ``session_key`` names a named
        profile: written by a multiplexer that had not yet learned to stamp the routed profile
        (#76423). Stores whose topic tables predate ``profile_name`` have nothing to relabel.c                óÌ   — | j                  d«      D �ch c]  }|d   ’Œ	 }}|rdt        | d«      vrg S | j                  d«      D �cg c]  }t        |«      ‘Œ c}S c c}w c c}w )NzYSELECT name FROM sqlite_master WHERE type='table' AND name = 'telegram_dm_topic_bindings'r   r1   Útelegram_dm_topic_bindingsz�SELECT chat_id, thread_id, session_key FROM telegram_dm_topic_bindings WHERE profile_name = 'default' ORDER BY chat_id, thread_id)r$   r(   rA   )r%   r'   Úexistings      r   Ú_readzNSessionProfileRepairMixin.find_profile_less_telegram_topic_rows.<locals>._reado   s€   € Ø*.¯,©,Økó+mö n 3˜˜A›ð nˆHð ná˜~´^ÀDÐJfÓ5gÑgØ�	Ø)-¯©ðMó*Nö O #”D˜•Iò Oð Oùò	nùòOs   ”AÁA!r   ©NÚdefaultr2   )Ú_read_retrying_ioerrr   r8   )r:   rG   Úoutr'   r2   s        r   Ú%find_profile_less_telegram_topic_rowsz?SessionProfileRepairMixin.find_profile_less_telegram_topic_rowsk   sc   € ò	Oð ˆØ×,Ñ,¨UÓ3ò 	@ˆCÜ-¨c°-Ñ.@ÓAˆKØÐ"3Ò3Ø—
‘
Ð>˜cÐ> =°+Ñ>Õ?ð	@ð ˆ
r   c                óL   — | j                  d|f«      }|rt        |d   «      S dS )Nz2SELECT COUNT(*) FROM messages WHERE session_id = ?r   )Ú	_read_oner9   )r:   Ú
session_idr'   s      r   Úcount_messages_allz,SessionProfileRepairMixin.count_messages_all~   s,   € Ø�n‰nÐQÐT^ÐS`ÓaˆÙ!Œs�3�q‘6‹{Ð( qÐ(r   c                óN   ‡— t        |«      Š‰sydˆfd„}| j                  |«      S )zŽSet ``profile_name`` to the profile in each row's own key. Re-derives the target inside the
        transaction (never trusts a stale report).r   c           	     óú   •— d}t        ‰«      D ]i  }| j                  dt        |«      › d�|«      j                  «       }|D ]5  \  }}t	        |«      }|€Œ|| j                  d|||f«      j
                  z  }Œ7 Œk |S )Nr   z2SELECT id, session_key FROM sessions WHERE id IN (ú)zKUPDATE sessions SET profile_name = ? WHERE id = ? AND profile_name IS NOT ?©r   r$   Ú_session_ids_placeholdersÚfetchallr   Úrowcount)r%   ÚchangedÚchunkr<   rO   r   ÚtargetÚwanteds          €r   Ú_dozFSessionProfileRepairMixin.relabel_sessions_to_key_profile.<locals>._do‹   s¢   ø€ ØˆGÜ# FÓ+ò 
?�Ø—|‘|ØHÔIbÐchÓIiÐHjÐjkÐlØóç#™8›:ð ð 04ò ?Ñ+�J Ü0°Ó=�FØ�~Ø Ø˜tŸ|™|ØeØ ¨VÐ4ó 6ç6>±hñ?‘Gñ	?ð	
?ð ˆNr   ©Úreturnr9   ©ÚlistÚ_execute_write©r:   Úidsr\   r[   s      @r   Úrelabel_sessions_to_key_profilez9SessionProfileRepairMixin.relabel_sessions_to_key_profile„   s,   ø€ ô �c“ˆÙØõ	ð ×"Ñ" 3Ó'Ð'r   c                óN   ‡— t        |«      Š‰sydˆfd„}| j                  |«      S )uï   Detach children from a parent keyed under another profile. Only ``parent_session_id`` is
        cleared â€” ``profile_name`` stays the row's own (relabelled separately when it is wrong) â€” and
        only while the crossing still holds.r   c                ó   •— d}t        ‰	«      D ]|  }| j                  dt        |«      › d�|«      j                  «       }|D ]H  \  }}}t	        |«      t	        |«      }}|€Œ |€Œ#||k7  sŒ)|| j                  d|f«      j
                  z  }ŒJ Œ~ |S )Nr   zwSELECT s.id, s.session_key, p.session_key FROM sessions s JOIN sessions p ON p.id = s.parent_session_id WHERE s.id IN (rS   z9UPDATE sessions SET parent_session_id = NULL WHERE id = ?rT   )
r%   ÚseveredrY   r<   rO   ÚkeyÚ
parent_keyÚmineÚtheirsr[   s
            €r   r\   z<SessionProfileRepairMixin.sever_crossed_parents.<locals>._do£   sÅ   ø€ ØˆGÜ# FÓ+ò 	q�Ø—|‘|ð&ä&?ÀÓ&FÐ%GÀqðJàKPóR÷ S[ÑRZÓR\ð ð 48ò qÑ/�J  ZÜ#6°sÓ#;Ô=PÐQ[Ó=\˜&�DØÑ'¨FÑ,>À4È6Ã>Ø 4§<¡<ØWÐZdÐYfó$hßhpÑhpñq™ñqð	qð ˆNr   r]   r_   rb   s      @r   Úsever_crossed_parentsz/SessionProfileRepairMixin.sever_crossed_parents›   s,   ø€ ô �c“ˆÙØõ	ð ×"Ñ" 3Ó'Ð'r   c                ó0   ‡— ˆfd„}| j                  |«      S )uö   Everything the target store needs to hold *session_id* as its own: the row (every column),
        the resolved system prompt, EVERY message row (inactive and compacted generations included â€”
        a move is not an export) and its usage rows.c                óŒ  •— | j                  d‰f«      j                  «       }|€y d }|d   r-| j                  d|d   f«      j                  «       }|r|d   nd }| j                  d‰f«      D �cg c]  }t        |«      ‘Œ }}| j                  d‰f«      D �cg c]  }t        |«      ‘Œ }}t        |«      |||dœS c c}w c c}w )Nz#SELECT * FROM sessions WHERE id = ?Úsystem_prompt_hashz0SELECT prompt FROM system_prompts WHERE hash = ?r   z7SELECT * FROM messages WHERE session_id = ? ORDER BY idz6SELECT * FROM session_model_usage WHERE session_id = ?)ÚsessionÚsystem_promptÚmessagesÚusage)r$   ÚfetchonerA   )r%   rp   Úpromptr'   Úrrr   rs   rO   s          €r   rG   z@SessionProfileRepairMixin.export_session_for_move.<locals>._read¸   sî   ø€ Ø—l‘lÐ#HÈ:È-ÓX×aÑaÓcˆGØˆØØˆFØÐ+Ò,Ø—l‘lØFÈÐQeÑIfÐHhójßjrÑjrÓjtð á#&˜˜Qš¨D�Ø)-¯©ØIÈJÈ=ó*Zö [ Aœ˜Q�ð [ˆHð [à&*§l¡lØHÈ:È-ó'Yö Z ”T˜!•Wð ZˆEð Zä# G›}¸vÐS[ÐfkÑlÐlùò	[ùòZs   Á/B<ÂC)rJ   )r:   rO   rG   s    ` r   Úexport_session_for_movez1SessionProfileRepairMixin.export_session_for_move´   s   ø€ ô	mð ×(Ñ(¨Ó/Ð/r   c               óh   ‡ ‡‡‡‡— t        ‰d   «      Š‰d   Šdˆˆˆ ˆˆfd„}‰ j                  |«      S )uƒ  Insert a moved session into THIS store as *profile_name*'s. ``present`` when the id already
        exists (an earlier run copied but did not delete), else ``imported``. The parent link survives
        only when the parent is already here â€” a moved row must never point across stores or at a
        row of another profile. Columns the target schema lacks are dropped, never invented.rp   r)   c                ó4  •— | j                  d‰f«      j                  «       ry‰‰d<   ‰j                  d«      }|r&| j                  d|f«      j                  «       €d ‰d<   ‰j                  | ‰j                  d«      «      ‰d<   ‰j	                  | d‰t        «       ¬«       ‰j                  d	«      xs g D ]!  }‰j	                  | d	i |¥d
‰i¥t        ¬«       Œ# ‰j                  d«      xs g D ]%  }‰j	                  | di |¥d
‰i¥t        «       ¬«       Œ' y)Nú#SELECT 1 FROM sessions WHERE id = ?Úpresentr1   r5   rq   ro   Úsessions)Úskiprr   rO   rs   Úsession_model_usageÚimported)r$   rt   ÚgetÚ_store_system_promptÚ_insert_rowÚ	frozensetÚ_MESSAGE_MOVE_SKIP)	r%   Ú	parent_idÚmessagers   Úpayloadr1   r:   rp   rO   s	       €€€€€r   r\   z;SessionProfileRepairMixin.import_moved_session.<locals>._doÐ   s0  ø€ Ø�|‰|ÐAÀJÀ=ÓQ×ZÑZÔ\Ø Ø&2ˆG�NÑ#ØŸ™Ð$7Ó8ˆIÙ˜TŸ\™\Ð*OÐR[ÐQ]Ó^×gÑgÓiÐqØ/3�Ð+Ñ,Ø,0×,EÑ,EÀdÈGÏKÉKÐXgÓLhÓ,iˆGÐ(Ñ)Ø×Ñ˜T :¨w¼Y»[ÐÔIØ"Ÿ;™; zÓ2Ò8°bò s�Ø× Ñ   zÐ3X°gÐ3X¸|ÈZÑ3XÔ_qÐ Õrðsà Ÿ™ WÓ-Ò3°ò u�Ø× Ñ  Ð'<Ð>aÀÐ>aÈÐV`Ñ>aÔhqÓhsÐ Õtðuàr   )r^   r   )rA   ra   )r:   r‡   r1   r\   rp   rO   s   ``` @@r   Úimport_moved_sessionz.SessionProfileRepairMixin.import_moved_sessionÈ   s:   ü€ ô
 �w˜yÑ)Ó*ˆØ˜T‘]ˆ
÷	ñ 	ð ×"Ñ" 3Ó'Ð'r   c               ó  — t        | |«      D �cg c]  }||v sŒ||vsŒ|‘Œ }}| j                  d|› ddj                  |«      › ddj                  d„ |D «       «      › d�|D �cg c]  }||   ‘Œ	 c}«       y c c}w c c}w )NzINSERT INTO z (z, z
) VALUES (c              3  ó    K  — | ]  }d –— Œ y­w)ú?N© )Ú.0Ú_s     r   ú	<genexpr>z8SessionProfileRepairMixin._insert_row.<locals>.<genexpr>ä   s   è ø€ ÒKaÐTUÌCÑKaùs   ‚rS   )r(   r$   Újoin)r%   r&   Úvaluesr}   ÚcÚcolumnss         r   r‚   z%SessionProfileRepairMixin._insert_rowà   s†   € ä,¨T°5Ó9Ö[˜¸QÀ&º[ÈQÐVZÊ]’1Ð[ˆÐ[Ø�‰Ø˜5˜'  D§I¡I¨gÓ$6Ð#7°zÀ$Ç)Á)ÑKaÐY`ÔKaÓBaÐAbÐbcÐdØ 'Ö(˜1ˆV�A‹YÒ(õ	*ùò \ùò )s   �	A7™A7žA7Á#A<
c                óH   ‡ ‡— dˆ ˆfd„}t        ‰ j                  |«      «      S )zÐRemove a session this store no longer owns after the target confirmed it. Children left
        behind are detached (same FK rule as :meth:`delete_session`); topic bindings on the row
        cascade with it.c                ó  •— | j                  d‰f«      j                  «       €y| j                  d‰f«       | j                  d‰f«       | j                  d‰f«       | j                  d‰f«       ‰j                  | «       y)Nrz   FzHUPDATE sessions SET parent_session_id = NULL WHERE parent_session_id = ?z)DELETE FROM messages WHERE session_id = ?z4DELETE FROM session_model_usage WHERE session_id = ?z!DELETE FROM sessions WHERE id = ?T)r$   rt   Ú#_delete_unreferenced_system_prompts)r%   r:   rO   s    €€r   r\   z;SessionProfileRepairMixin.delete_moved_session.<locals>._doë   s|   ø€ Ø�|‰|ÐAÀJÀ=ÓQ×ZÑZÓ\ÐdØØ�L‰LÐcÐfpÐerÔsØ�L‰LÐDÀzÀmÔTØ�L‰LÐOÐR\ÐQ^Ô_Ø�L‰LÐ<¸z¸mÔLØ×4Ñ4°TÔ:Ør   )r^   Úbool)r—   ra   )r:   rO   r\   s   `` r   Údelete_moved_sessionz.SessionProfileRepairMixin.delete_moved_sessionç   s    ù€ ö	ô �D×'Ñ'¨Ó,Ó-Ð-r   c                óx   ‡‡‡— ddl m} t        |«      Š‰sy |‰«      dz   Šdˆˆˆfd„}| j                  |«      S )u  ``agent:main:â€¦`` â†’ ``agent:<profile>:â€¦`` (+ ``profile_name``) for rows a standalone
        gateway wrote before this store's profile was multiplexed (#113884). Only rows still under
        the legacy namespace change; a ``main``-named profile is written in its marked form.r   )Ú_session_key_namespacer   c                óŽ   •— d}t        ‰«      D ]3  }|| j                  dt        |«      › d�‰‰g|¢­«      j                  z  }Œ5 |S )Nr   z^UPDATE sessions SET session_key = ? || substr(session_key, 12), profile_name = ? WHERE id IN (z0) AND substr(session_key, 1, 11) = 'agent:main:')r   r$   rU   rW   )r%   rX   rY   Únew_nsÚprofiler[   s      €€€r   r\   zASessionProfileRepairMixin.rekey_legacy_main_sessions.<locals>._do  se   ø€ ØˆGÜ# FÓ+ò 8�Ø˜4Ÿ<™<ð$Ü$=¸eÓ$DÐ#EÐEuðwà˜WÐ- uÑ-ó/÷ 08©xñ8‘ð8ð
 ˆNr   r]   )r   rš   r`   ra   )r:   rc   r�   rš   r\   rœ   r[   s     `  @@r   Úrekey_legacy_main_sessionsz4SessionProfileRepairMixin.rekey_legacy_main_sessionsø   s>   ú€ õ 	;Ü�c“ˆÙØÙ'¨Ó0°3Ñ6ˆ÷	ð ×"Ñ" 3Ó'Ð'r   c                óN   ‡— t        |«      Š‰sydˆfd„}| j                  |«      S )Nr   c                ó.   •‡ — t        ˆ fd„‰D «       «      S )Nc              3  ó`   •K  — | ]%  \  }}‰j                  d ||f«      j                  –— Œ' y­w)z?DELETE FROM gateway_routing WHERE scope = ? AND session_key = ?N©r$   rW   )r�   Úscoperh   r%   s      €r   r�   zUSessionProfileRepairMixin.delete_gateway_routing_rows.<locals>._do.<locals>.<genexpr>  s?   øè ø€ ò *á�E˜3ð —|‘|ØQÐTYÐ[^ÐS_óaßaiÑaiójñ *ùs   ƒ+.©Úsum©r%   r[   s   `€r   r\   zBSessionProfileRepairMixin.delete_gateway_routing_rows.<locals>._do  s   ù€ Üó *à"(ô*ó *ð *r   r]   r_   ©r:   r<   r\   r[   s      @r   Údelete_gateway_routing_rowsz5SessionProfileRepairMixin.delete_gateway_routing_rows  s*   ø€ Ü�d“ˆÙØõ	*ð ×"Ñ" 3Ó'Ð'r   c                óN   ‡— t        |«      Š‰sydˆfd„}| j                  |«      S )u·   Adopt ``(scope, session_key, entry_json, updated_at)`` rows another store held for this
        one's routing index. An existing key wins â€” the row the gateway actually loads stays.r   c                ó.   •‡ — t        ˆ fd„‰D «       «      S )Nc              3  óV   •K  — | ]   }‰j                  d |«      j                  –— Œ" y­w)zfINSERT OR IGNORE INTO gateway_routing (scope, session_key, entry_json, updated_at) VALUES (?, ?, ?, ?)Nr¢   )r�   r'   r%   s     €r   r�   z_SessionProfileRepairMixin.insert_gateway_routing_rows_if_absent.<locals>._do.<locals>.<genexpr>  s6   øè ø€ ò Hà9<ð —|‘|ð&Ø'*ó,ç,4©Hó5ñ Hùs   ƒ&)r¤   r¦   s   `€r   r\   zLSessionProfileRepairMixin.insert_gateway_routing_rows_if_absent.<locals>._do  s!   ù€ Üó Hà@FôHó Hð Hr   r]   r_   r§   s      @r   Ú%insert_gateway_routing_rows_if_absentz?SessionProfileRepairMixin.insert_gateway_routing_rows_if_absent  s-   ø€ ô �d“ˆÙØõ	Hð ×"Ñ" 3Ó'Ð'r   c                ó`   ‡‡— t        |«      ŠddddœŠ‰s‰S dˆˆfd„}| j                  |«      S )zóStamp the key's profile onto ``default``-labelled bindings (and the chat's mode row when
        only the default one exists). A binding that would collide with one the named profile already
        wrote is the stale duplicate and is removed.r   )Úbindings_relabelledÚbindings_duplicates_removedÚmode_rows_relabelledc           
     óü  •— dt        | d«      v }‰D ]ç  }t        |d   «      }|dv rŒ|d   |d   }}| j                  d|||f«      j                  «       }|r*‰dxx   | j                  d	||f«      j                  z  cc<   n*‰d
xx   | j                  d|||f«      j                  z  cc<   |sŒœ| j                  d||f«      j                  «       �Œ¿‰dxx   | j                  d||f«      j                  z  cc<   Œé ‰S )Nr1   Útelegram_dm_topic_moder   rH   Úchat_idÚ	thread_idzaSELECT 1 FROM telegram_dm_topic_bindings WHERE profile_name = ? AND chat_id = ? AND thread_id = ?r¯   zgDELETE FROM telegram_dm_topic_bindings WHERE profile_name = 'default' AND chat_id = ? AND thread_id = ?r®   zwUPDATE telegram_dm_topic_bindings SET profile_name = ? WHERE profile_name = 'default' AND chat_id = ? AND thread_id = ?zKSELECT 1 FROM telegram_dm_topic_mode WHERE profile_name = ? AND chat_id = ?r°   zaUPDATE telegram_dm_topic_mode SET profile_name = ? WHERE profile_name = 'default' AND chat_id = ?)r(   r   r$   rt   rW   )	r%   Úmode_has_profiler'   rZ   r³   r´   ÚcollidesÚcountsr[   s	          €€r   r\   zBSessionProfileRepairMixin.relabel_telegram_topic_rows.<locals>._do/  sF  ø€ Ø-´ÀÐF^Ó1_Ð_ÐØò G�Ü,¨S°Ñ-?Ó@�ØÐ.Ñ.ØØ%(¨¡^°S¸Ñ5E˜�ØŸ<™<ð(Ø*0°'¸9Ð)EóGçGOÁxÃzð ñ ØÐ8Ó9¸T¿\¹\ð<Ø>EÀyÐ=Qó>SçS[ÑS[ñ\Ô9ð Ð0Ó1°T·\±\ð<Ø>DÀgÈyÐ=Yó6[ç[cÑ[cñdÓ1ò $¨¯©ØeØ Ð)ó)+ç+3©8«:ñ)>ð Ð1Ó2°d·l±lð*Ø,2°GÐ+<ó7>ç>F¹hñGÔ2ð'Gð, ˆMr   )r^   úDict[str, int]r_   )r:   r<   r\   r·   r[   s      @@r   Úrelabel_telegram_topic_rowsz5SessionProfileRepairMixin.relabel_telegram_topic_rows&  s;   ù€ ô �d“ˆØ)*È1ÐfgÑhˆÙØˆMö	ð2 ×"Ñ" 3Ó'Ð'r   c                óz   — | j                  d|t        |«      f«      }d„ |D «       D �ch c]  }|€Œ|’Œ	 c}S c c}w )už   Profiles whose keyed sessions hold *(platform, chat_id)* in this store â€” the evidence for
        who owns a profile-less ``gateway_voice_mode.json`` entry.ziSELECT DISTINCT session_key FROM sessions WHERE source = ? AND chat_id = ? AND session_key LIKE 'agent:%'c              3  ó8   K  — | ]  }t        |d    «      –— Œ y­w)r   N)r   )r�   rv   s     r   r�   zBSessionProfileRepairMixin.key_profiles_for_chat.<locals>.<genexpr>R  s   è ø€ ÒPÀaÔ/°°-Ñ0@×AÑPùs   ‚)r7   r   )r:   Úplatformr³   r<   Úps        r   Úkey_profiles_for_chatz/SessionProfileRepairMixin.key_profiles_for_chatL  sF   € ð �~‰~ð-Ø/7¼¸W»Ð.FóHˆñ QÈ4ÔPÖb�aÐTUÑTa’ÒbÐbùÒbs   ©8±8N)r;   r   r^   zDict[str, List[Dict[str, Any]]])r^   zList[Dict[str, Any]])rO   r   r^   r9   )rc   úIterable[str]r^   r9   )rO   r   r^   zOptional[Dict[str, Any]])r‡   úDict[str, Any]r1   r   r^   r   )r&   r   r‘   rÀ   r}   rƒ   r^   ÚNone)rO   r   r^   r—   )rc   r¿   r�   r   r^   r9   )r<   zIterable[Tuple[str, str]]r^   r9   )r<   z%Iterable[Tuple[str, str, str, float]]r^   r9   )r<   zIterable[Dict[str, Any]]r^   r¸   )r¼   r   r³   r   r^   zSet[str])Ú__name__Ú
__module__Ú__qualname__Ú__doc__r?   rB   rL   rP   rd   rl   rw   rˆ   Ústaticmethodr‚   r˜   rž   r¨   r¬   r¹   r¾   rŒ   r   r   r,   r,   =   sb   „ Ùhó"óH,ó
ó&)ó(ó.(ó20ó((ð0 ò*ó ð*ó.ó"(ó(	(ó(ó"(ôLcr   r,   )r   r   r^   úOptional[str])r    r   r^   rÇ   )r&   r   r^   z	List[str])rÅ   Ú
__future__r   ÚloggingÚtypingr   r   r   r   r   r	   r
   Úhermes_state_commonr   r   rU   Ú	getLoggerrÂ   Úloggerr   r"   r(   rƒ   r„   r,   rŒ   r   r   ú<module>rÎ      s`   ðñõ* #ã ß B× BÑ Bç Và	ˆ×	Ñ	˜8Ó	$€ó	8óóMñ   oÐ6Ó7Ð ÷Ucò Ucr   