+
    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	 ^ RI
Ht ^ RIHtHtHtHt ]'       d	   ^ RIHtHt ]P(                  ! R4      tR	 R
 lt ! R R4      tR# )a  SessionStore durable-row recovery: session-key generation, legacy Slack key migration, rebuilding
a routing entry from state.db, and the SQLite side of routing transitions (promote/reopen/create/
peer). Mixin split out of ``gateway/session.py``; bound onto ``SessionStore`` via the MRO.)ÚannotationsN)Úreplace)Údatetime)ÚPlatform)ÚTYPE_CHECKINGÚAnyÚDictÚOptional)ÚSessionEntryÚSessionSourcezgateway.sessionc               ó   € V ^8„  d   QhRR/# )é   ÚreturnúOptional[str]© )Úformats   "Ú=/home/ubuntu/.hermes/hermes-agent/gateway/session_recovery.pyÚ__annotate__r      s   € ÷ ñ ˜Mñ ó    c                ó|   € V f   R#  \         P                  ! V P                  4       4      #   \         d     R# i ; i)zA``source.to_dict()`` as JSON, or None when absent/unserializable.N)ÚjsonÚdumpsÚto_dictÚ	Exception)Úsources   &r   Ú_origin_jsonr      s7   € à‚~ÙðÜ�zŠz˜&Ÿ.™.Ó*Ó+Ð+øÜô Úðús   ˆ#, ¬;º;c                  ó’  € ] tR t^!tRtR5R R llt]R R l4       t]R R l4       tR	 R
 lt	R5R R llt
R R ltR R lt]R R l4       tR R ltRR/R R llt]RR/R R ll4       tRR/R R lltRR/R R lltR  tRR/R! R" lltR# R$ ltR%R&/R' R( lltR6R) R* lltR+ R, ltR-R&/R. R/ llt]R0 R1 l4       tR2 R3 ltR4tR# )7ÚSessionRecoveryMixinzMSessionStore durable-row recovery and the SQLite side of routing transitions.Nc               ó    € V ^8„  d   QhRRRR/# )r   r   úOptional[SessionSource]r   r   r   )r   s   "r   r   Ú!SessionRecoveryMixin.__annotate__$   s   € ÷ ñ Ð/Fð ÐR_ñ r   c                ó  € \        V P                  RR4      '       g   R# ^ RIHp V! V4      pVe   VP                  # Ve   VP
                  '       d   VP
                  #  ^ RIHp V! 4       ;'       g    R#   \         d     R# i ; i)z°Profile namespace for session keys: None when multiplexing is off (legacy
``agent:main``), else the pinned identity's runtime profile, ``source.profile`` or the
active profile.Úmultiplex_profilesFN)Úidentity_of©Úget_active_profile_nameÚdefault)	ÚgetattrÚconfigÚgateway.session_identityr#   Úsession_key_profileÚprofileÚhermes_cli.profilesr%   r   )Úselfr   r#   Úidentityr%   s   &&   r   Ú_resolve_profile_for_keyÚ-SessionRecoveryMixin._resolve_profile_for_key$   s}   € ô �t—{‘{Ð$8¸%×@Ò@ÙÝ8Ù˜vÓ&ˆØÒØ×/Ñ/Ð/ØÒ &§.§. .Ø—>‘>Ð!ð	ÝCÙ*Ó,×9Ð9°	Ð9øÜô 	Úð	ús   Á!A8 Á5A8 Á8BÂBc               ó    € V ^8„  d   QhRRRR/# )r   Úsession_keyr   r   r   )r   s   "r   r   r    7   s   € ÷ 5ñ 5¨}ð 5Àñ 5r   c                ó   € ^ RI Hp V! V 4      # )z?Extract the profile namespace encoded in a gateway session key.)Úprofile_from_session_key)Úhermes_state_keysr4   )r2   r4   s   & r   Ú_profile_from_session_keyÚ.SessionRecoveryMixin._profile_from_session_key6   s   € õ 	?Ù'¨Ó4Ð4r   c               ó   € V ^8„  d   QhRR/# )r   r   Ústrr   )r   s   "r   r   r    =   s   € ÷ ñ  #ñ r   c                 	óV   €  ^ RI Hp  V ! 4       ;'       g    R#   \         d     R# i ; i)é    r$   r&   )r,   r%   r   r$   s    r   Ú_active_profile_nameÚ)SessionRecoveryMixin._active_profile_name<   s-   € ð	ÝCÙ*Ó,×9Ð9°	Ð9øÜô 	Úð	ús   ‚ – ™(§(c               ó$   € V ^8„  d   QhRRRRRR/# )r   Úrequested_session_keyr9   Ú	recoveredúdict[str, Any]r   Úboolr   )r   s   "r   r   r    D   s)   € ÷ @ñ @Ø(+ð@Ø8Fð@à	ñ@r   c               ó8  € \        VP                  R4      ;'       g    R4      pV'       d   W18X  d   R# V P                  V4      pVf   R# \        V P                  RR4      '       d"   V P                  V4      pVRJ ;'       g    WE8H  # W@P                  4       8H  # )uJ  Prevent a gateway from reviving another profile's row. Single-profile: the row's
namespace must match the ACTIVE profile. Multiplexed: it must match the requested key's
namespace (the active profile is meaningless there). Keyless rows stay adoptable.

Multiplexed: several profiles serve traffic at once, so the active profile is meaningless â€” the
requested key carries the profile the turn was routed to, and the recovered row must sit in the same
``agent:<ns>:`` namespace (#74285). Rows with no key namespace stay adoptable in both modes
(legacy/keyless data owned by this store).
r2   Ú TNr"   F)r9   Úgetr6   r'   r(   r<   )r-   r?   r@   Úrecovered_keyÚrecovered_profileÚrequested_profiles   &$$   r   Ú)_recovered_row_allowed_for_active_profileÚ>SessionRecoveryMixin._recovered_row_allowed_for_active_profileD   s“   € ô ˜IŸM™M¨-Ó8×>Ð>¸BÓ?ˆß Ô FÙØ ×:Ñ:¸=ÓIÐØÒ$ÙÜ�4—;‘;Ð 4°e×<Ò<Ø $× >Ñ >Ð?TÓ UÐØ$¨Ð,×VÐVÐ0AÑ0VÐVØ ×$=Ñ$=Ó$?Ñ?Ð?r   c               ó$   € V ^8„  d   QhRRRRRR/# )r   r   r   Ú
key_sourcer   r   r9   r   )r   s   "r   r   r    [   s#   € ÷ ;ñ ;¨Mð ;ÐG^ð ;Ðjmñ ;r   c           	     ó¤   € ^ RI Hp T! Ve   TMT\        V P                  RR4      \        V P                  RR4      V P	                  V4      R7      # )zQSession key for *source* (profile from *source*; key from *key_source* if given).)Úbuild_session_keyÚgroup_sessions_per_userTÚthread_sessions_per_userF)rO   rP   r+   )Úgateway.sessionrN   r'   r(   r/   )r-   r   rL   rN   s   &&& r   Ú_generate_session_keyÚ*SessionRecoveryMixin._generate_session_key[   sN   € å5Ù Ø$Ò0‰J°fÜ$+¨D¯K©KÐ9RÐTXÓ$YÜ%,¨T¯[©[Ð:TÐV[Ó%\Ø×1Ñ1°&Ó9ô	;ð 	;r   c               ó    € V ^8„  d   QhRRRR/# )r   r   r   r   r   r   )r   s   "r   r   r    d   s   € ÷ añ a°ð aÀ-ñ ar   c           	     ó¤   € VP                   \        P                  8w  g   VP                  '       g   R# V P	                  V\        VRRR7      4      # )z­Pre-workspace Slack key for an explicitly scoped source. Deliberately Slack-only: an
unscoped Slack session may be claimed by only one workspace (old key cannot tell teams).N)Úscope_idÚguild_id)Úplatformr   ÚSLACKrV   rR   r   )r-   r   s   &&r   Ú_legacy_slack_session_keyÚ.SessionRecoveryMixin._legacy_slack_session_keyd   s<   € ð �?‰?œhŸn™nÔ,°F·O·O°OÙØ×)Ñ)¨&´'¸&È4ÐZ^Ô2_Ó`Ð`r   c               ó    € V ^8„  d   QhRRRR/# )r   Ú
legacy_keyr   r   rB   r   )r   s   "r   r   r    k   s   € ÷ 	ñ 	°-ð 	ÀDñ 	r   c                ó  € V'       g   R# V P                  R\        P                  4      ;_uu_ 4        V P                  R\        4      pW9   d    RRR4       R# VP	                  V4        RRR4       R#   + '       g   i     R# ; i)z@Atomically reserve one ambiguous legacy Slack key for migration.FÚ_legacy_slack_claim_lockÚ_claimed_legacy_slack_keysNT)Ú_lazyÚ	threadingÚLockÚsetÚadd)r-   r]   Úclaimeds   && r   Ú_claim_legacy_slack_keyÚ,SessionRecoveryMixin._claim_legacy_slack_keyk   sb   € çÙØ�Z‰ZÐ2´I·N±N×CÕCØ—j‘jÐ!=¼sÓCˆGØÔ$Ø÷ DÑCð �K‰K˜
Ô#Ø÷ D×C×CÒCús   ³A7ÁA7Á7B	c               ó$   € V ^8„  d   QhRRRRRR/# )r   r@   rA   r   r   r   rB   r   )r   s   "r   r   r    w   s)   € ÷ Qñ QØ!ðQØ+8ðQà	ñQr   c                ó¦  € VP                   \        P                  8w  g$   VP                  R8X  g   VP                  '       g   R#  \
        P                  ! V P                  R4      ;'       g    R4      p\        T\        4      '       g   R# TP                  RTP                  R4      4      TP                  8H  #   \        \        3 d     R# i ; i)a  Reject recovered rows whose origin belongs to another workspace: a workspace-scoped Slack
lookup adopts a row only if its origin_json names the same scope_id; rows without a
parseable origin are rejected (an unattributable transcript is exactly the ambiguity).ÚdmTÚorigin_jsonrD   FrV   rW   )rX   r   rY   Ú	chat_typerV   r   ÚloadsrE   Ú	TypeErrorÚ
ValueErrorÚ
isinstanceÚdict)r@   r   Úorigins   && r   Ú#_recovered_row_matches_source_scopeÚ8SessionRecoveryMixin._recovered_row_matches_source_scopev   s¢   € ð �?‰?œhŸn™nÔ,°×0@Ñ0@ÀDÔ0HÐPV×P_×P_ÐP_Ùð	Ü—Z’Z 	§¡¨mÓ <× BÐ BÀÓCˆFô ˜&¤$×'Ò'ÙØ�z‰z˜* f§j¡j°Ó&<Ó=ÀÇÁÑPÐPøô	 œ:Ð&ô 	Úð	ús   Á'B; Á.B; Â;CÃCc          
     ó,   € V ^8„  d   QhRRRRRRRRR	R
/# )r   ÚrowrA   r2   r9   r   r   Únowr   r   r
   r   )r   s   "r   r   r    ‡   s;   € ÷ cñ cØ$ðcØ36ðcØ@MðcØT\ðcà	ñcr   c               	óâ  € ^ RI Hp R R lpV! VP                  R4      \        P                  ! ^ 4      4      pVP                  R4      pVe	   V! W‡4      MTp	VP                  R4      p
V
f1   \        VP                  R4      ;'       g    ^ 4      ;'       g    VRJp
^ R	IHp V! V\        VR
,          4      VW“VP                  VP                  VP                  \        V
4      V! V4      R7
      # )r;   )r
   c               ó    € V ^8„  d   QhRRRR/# )r   r&   r   r   r   )r   s   "r   r   ÚKSessionRecoveryMixin._create_entry_from_recovered_row.<locals>.__annotate__Œ   s   € ÷ 	ñ 	 ð 	¨Xñ 	r   c                ó~   €  \         P                  ! \        V 4      4      #   \        \        \
        3 d    Tu # i ; i©N)r   ÚfromtimestampÚfloatro   rp   ÚOSError)Úvaluer&   s   &&r   Ú_tsÚBSessionRecoveryMixin._create_entry_from_recovered_row.<locals>._tsŒ   s7   € ðÜ×-Ò-¬e°E«lÓ;Ð;øÜœz¬7Ð3ô Ø’ðús   ‚! ¡<»<Ú
started_atÚlast_activity_atNÚ_has_messagesÚmessage_count©Útransport_profile_ofÚid)
r2   Ú
session_idÚ
created_atÚ
updated_atrs   Údisplay_namerX   rm   Úreset_had_activityÚtransport_profile)rQ   r
   rE   r   r~   rB   r)   r‰   r9   Ú	chat_namerX   rm   )r-   rw   r2   r   rx   r
   r‚   rŒ   Úlast_activityr�   Úhad_activityr‰   s   &$$$$       r   Ú _create_entry_from_recovered_rowÚ5SessionRecoveryMixin._create_entry_from_recovered_row‡   sØ   € õ 	1õ	ñ ˜Ÿ™ Ó.´×0FÒ0FÀqÓ0IÓJˆ
àŸ™Ð 2Ó3ˆØ7DÒ7P‘S˜Ô3ÐV`ˆ
Ø—w‘w˜Ó/ˆØÒÜ §¡¨Ó 8× =Ð =¸AÓ>×[Ð[À-ÐW[ÐB[ˆLÝAÙØ#´°C¸µI³È:Ø!¸v×?OÑ?OØ—_‘_°×0@Ñ0@Ü# LÓ1ÑEYÐZ`ÓEaô	cð 	cr   Úraise_on_lookup_errorFc          
     ó,   € V ^8„  d   QhRRRRRRRRRR	/# )
r   r2   r9   r   r   Úallow_peer_fallbackrB   r–   r   úOptional[dict[str, Any]]r   )r   s   "r   r   r    ¡   s;   € ÷ Uñ UØ!ðUØ+8ðUØOSðUà#ðUà0HñUr   c               óò   € T P                  V P                  V4      VP                  P                  TVP                  V'       d   VP
                  MRV'       d   VP                  MRVP                  VR7      # )a  Query one durable gateway session row. Scoped Slack lookups disable SessionDB's
platform/chat/user fallback: that tuple has no workspace id and could revive another team's
session; the caller performs one explicit exact lookup of the old unscoped key instead.N)r   r2   Úuser_idÚchat_idrm   Ú	thread_idr–   )Ú	_peer_rowÚ_db_for_keyrX   r�   r›   rœ   rm   r�   )r-   r2   r   r˜   r–   s   &$$$$r   Ú_find_gateway_session_rowÚ.SessionRecoveryMixin._find_gateway_session_row¡   sd   € ð �~‰~Ø×Ñ˜[Ó)°&·/±/×2GÑ2GÐU`Ø—N‘Nß&9�F—N’N¸tß*=�f×&Ò&À4Ø×&Ñ&Ð>Sð ó Uð 	Ur   c          
     ó,   € V ^8„  d   QhRRRRRRRRRR	/# )
r   r   r9   r2   r–   rB   Úpeerr   r   r™   r   )r   s   "r   r   r    ¯   s1   € ÷ ñ  ð °3ð Ètð ØðØ":ñr   c               óà   € V '       d   \        V RR4      MRp\        V4      '       g   R#  V! RRVRV/VB #   \         d+   p\        P	                  RY&4       T'       d   h  Rp?R# Rp?ii ; i)a  ``db.find_latest_gateway_session_for_peer`` guarded for a missing store, a SessionDB
without the finder, and a failing lookup (debug-logged -> None unless *raise_on_lookup_error*).
Extra keyword arguments (user_id/chat_id/chat_type/thread_id) pass through to the finder.Ú$find_latest_gateway_session_for_peerNr   r2   z-Gateway session DB recovery failed for %s: %sr   )r'   Úcallabler   ÚloggerÚdebug)Údbr   r2   r–   r£   ÚfinderÚexcs   &$$$,  r   rž   ÚSessionRecoveryMixin._peer_row®   sm   € ÷ OQ”˜ÐCÀTÔJÐVZˆÜ˜×ÒÙð	ÙÑI ÐI°[ÐIÀDÑIÐIøÜô 	Ü�L‰LÐHÈ+Ô[ß$ØÝûð		ús   ¬8 ¸A-ÁA(Á(A-Ú	not_afterc               ó$   € V ^8„  d   QhRRRRRR/# )r   r2   r9   r­   zOptional[float]r   zOptional[tuple[str, Any]]r   )r   s   "r   r   r    ¿   s$   € ÷ '"ñ '"Øð'"Ø.=ð'"à	"ñ'"r   c               óR  € V'       g   R# V P                  V4      pVf   R# V P                  V4      pV'       d   WC3# \        V4      P                  R4      p\	        V4      ^8¼  d   V^ ,          R8X  d
   V^,          MRpV'       g   R# V P                  W6VR7      p\        V\        4      '       d   VP                  R4      '       g   R# VP                  R4      pVe4   Ve0   \        P                  ! \        V4      4      \        V4      8”  d   R# \        VR,          4      V3# )aµ  Resolve a gateway session key to ``(session_id, db)`` for shutdown-flush recovery.

The routing map (``peek_session_id``) is authoritative: it names the session the message
was actually routed to at shutdown. When ``sessions.json`` was pruned, fall back to the
durable row under the exact key (the peer finder keeps the reset fence: rows ended only by
recoverable reasons match; explicit boundaries do not). A row started after the flush
(``started_at > not_after``) cannot be the origin and is never adopted. Never mints a
session; None means the caller must preserve the flush file. ``db`` is the store owning
the key, so the append lands in the right profile partition. When that store cannot be
resolved (``_db_for_key`` fails closed for a profile without a reachable home) the answer
is None even if the routing map knows the id: appending to the ambient root store would
split one session identity across two physical stores (#66887/#102157).
NÚ:Úagent)r   r2   rŠ   r„   )rŸ   Úpeek_session_idr9   ÚsplitÚlenrž   rq   rr   rE   ÚmathÚfloorr   Úint)	r-   r2   r­   r©   r‹   ÚpartsrX   rw   r„   s	   &&$      r   Úresolve_session_id_for_keyÚ/SessionRecoveryMixin.resolve_session_id_for_key¿   sö   € ÷  ÙØ×Ñ˜kÓ*ˆØŠ:ÙØ×)Ñ)¨+Ó6ˆ
ßØ�>Ð!Ü�KÓ ×&Ñ& sÓ+ˆÜ" 5›z¨Qœ°5¸µ8¸wÔ3F�5˜–8ÈDˆßÙð �n‰n˜R¸kˆnÓJˆÜ˜#œt×$Ò$¨C¯G©G°D¯MªMÙØ—W‘W˜\Ó*ˆ
ð Ò  ZÒ%;ÄÇ
Â
Ì5ÐQ[ÓK\Ó@]Ô`cÐdmÓ`nÔ@nÙÜ�3�t•9‹~˜rÐ!Ð!r   c          
     ó,   € V ^8„  d   QhRRRRRRRRR	R
/# )r   r2   r9   r   r   rx   r   r–   rB   r   zOptional[SessionEntry]r   )r   s   "r   r   r    è   s4   € ÷ ñ Ø!ðØ+8ðØ?Gðà#ðà0Fñr   c               óÔ   € V P                  WVVR7      w  rVVf   R# V P                  WP                  4       V'       d)   V P                  VP                  WVP                  R7       V# )zERebuild a missing session-key mapping from a recoverable durable row.)r2   r   rx   r–   N©rŽ   )Ú_query_recoverable_rowÚ_reopen_session_rowr‹   Ú_record_gateway_session_peerrŽ   )r-   r2   r   rx   r–   ÚentryÚmigrated_legacys   &$$$$  r   Ú_recover_session_from_dbÚ-SessionRecoveryMixin._recover_session_from_dbè   ss   € ð "&×!<Ñ!<ð $¸Ø"7ð "=ó "9Ñˆð Š=ÙØ× Ñ  ×.>Ñ.>Ô?ßØ×-Ñ-Ø× Ñ  +ÀE×DVÑDVð .ô Xàˆr   c               ó˜   € V P                  WVR7      w  rEVe1   V'       d)   V P                  VP                  WVP                  R7       V# )a  DB-only half of _recover_session_from_db (no lock needed): a SessionEntry or None; the
caller assigns _entries[key] under lock. The row is NOT reopened here: the caller evaluates
reset policy first (an agent_close/ws_orphan row may need promotion to a real reset).)r2   r   rx   r½   )r¾   rÀ   r‹   rŽ   )r-   r2   r   rx   rÁ   rÂ   s   &$$$  r   Ú_query_recoverable_sessionÚ/SessionRecoveryMixin._query_recoverable_sessionú   sU   € ð "&×!<Ñ!<Ø#¸ð "=ó "=ÑˆàÒ§Ø×-Ñ-Ø× Ñ  +ÀE×DVÑDVð .ô Xàˆr   c               ó   € V ^8„  d   QhRR/# )r   r   z#tuple[Optional[SessionEntry], bool]r   )r   s   "r   r   r      s   € ÷ &ñ &à	,ñ&r   c               óî  € V P                  V4      pV P                  WVRJ VR7      pRpV'       g?   V'       d7   V P                  V4      '       d    V P                  WRRVR7      p\        V4      p\	        V\
        4      '       g   R# V P                  Wb4      '       g   R# V P                  WR7      '       g)   \        P                  RVP                  R4      V4       R# V P                  WaW#R7      pW‡3# )	zúFind and gate a recoverable row -> (entry or None, migrated_legacy). The legacy
(pre-workspace) Slack key fallback lives here: exact-key lookup, claimed once per process;
``migrated_legacy`` tells the caller to rewrite the peer row to the scoped key.N)r2   r   r˜   r–   F)r?   r@   z\Gateway session DB recovery ignored %s for %s because the row belongs to a different profiler2   )rw   r2   r   rx   )NF)rZ   r    rg   rB   rq   rr   rt   rI   r§   ÚwarningrE   r”   )	r-   r2   r   rx   r–   r]   r@   rÂ   rÁ   s	   &$$$$    r   r¾   Ú+SessionRecoveryMixin._query_recoverable_row  s  € ð ×3Ñ3°FÓ;ˆ
Ø×2Ñ2Ø#È
ÐVZÐHZØ"7ð 3ó 9ˆ	ð  ˆßŸZ¨D×,HÑ,HÈ×,TÒ,TØ×6Ñ6Ø&È5Ø&;ð 7ó =ˆIô # 9›oˆOÜ˜)¤T×*Ò*ØÐØ×7Ñ7¸	×JÒJØÐØ×=Ñ=Ø"-ð >÷ Dò Dä�N‰Nð$Ø%.§]¡]°=Ó%AÀ;ôPð ÐØ×5Ñ5Ø¸6ð 6ó LˆàÐ%Ð%r   c               ó(   € V ^8„  d   QhRRRRRRRR/# )r   r2   r9   r‹   Úreasonr   ÚNoner   )r   s   "r   r   r    #  s*   € ÷ ñ °#ð À3ð ÐPSð Ð`dñ r   c               ó<  €  V P                  V4      p\        VRR4      p\        V4      '       d
   V! W#4       MVP                  W#4       RV 2pVP	                  V4      '       d   VP                  VR4       R# R#   \         d   pT! T4        Rp?R# Rp?ii ; i)aT  End *session_id* with *reason* via ``promote_to_session_reset`` (``end_session`` on old
SessionDBs). Promote, not plain end: a row already ended with a recoverable accidental
reason (agent_close / ws_orphan_reap) must be upgraded to the explicit boundary, or
stale-route recovery resurrects it over the reset. ``log(exc)`` reports failures.Úpromote_to_session_resetNz
heartbeat:rD   )rŸ   r'   r¦   Úend_sessionÚget_metaÚset_metar   )	r-   r2   r‹   rÍ   Úlogr©   ÚpromoteÚheartbeat_keyr«   s	   &&&&$    r   Ú_promote_session_resetÚ+SessionRecoveryMixin._promote_session_reset#  sŽ   € ð
	Ø×!Ñ! +Ó.ˆBÜ˜bÐ"<¸dÓCˆGÜ˜× Ò Ù˜
Õ+à—‘˜zÔ2ð )¨¨Ð5ˆMØ�{‰{˜=×)Ò)Ø—‘˜M¨2Ö.ñ *øäô 	Ù��HŠHûð	ús   ‚A$A= Á'A= Á=BÂBÂBÚ
log_prefixrD   c               ó(   € V ^8„  d   QhRRRRRRRR/# )r   r2   r9   r‹   rÙ   r   rÎ   r   )r   s   "r   r   r    7  s0   € ÷ ^ñ ^¨sð ^Àð ^ÐTWð ^Ðaeñ ^r   c               óî   €  V P                  V4      P                  V4       R#   \         dF   pT'       d   \        P	                  RY44        Rp?R# \        P	                  RY4        Rp?R# Rp?ii ; i)z?Best-effort ``reopen_session``; failures are debug-logged only.z%s: %sz+Gateway session DB reopen failed for %s: %sN)rŸ   Úreopen_sessionr   r§   r¨   )r-   r2   r‹   rÙ   r«   s   &&&$ r   r¿   Ú(SessionRecoveryMixin._reopen_session_row7  sY   € ð	^Ø×Ñ˜[Ó)×8Ñ8¸ÖDøÜô 	^ßÜ—‘˜X z×7Ò7ä—‘ÐJÈK×]Ò]ûð		^ús   ‚ $ ¤A4¯A/ÁA/Á/A4c               ó4   € V ^8„  d   QhRRRRRRRRRR	R
RRR/# )r   r‹   r9   r2   r   r   rŽ   r   Úinclude_compression_ancestorsrB   r�   r   rÎ   r   )r   s   "r   r   r    A  sQ   € ÷ \ñ \Øð\Ø,/ð\Ø9Pð\à#ð\àKOð\ð )ð\ð 59ñ\r   c                óÔ  € V P                  V4      pV'       d	   V'       g   R# \        VRR4      p\        V4      '       g   R# ^ RIHp	 ^ RIHp
 ^ RIHp \        V
! WsW! WqVP                  P                  4      4      VP                  VVP                  VP                  VP                  R7      p T! V3/ VBRT;'       g    VP                   R\#        V4      R	TR
T;'       g	    V! V4      /B  R#   \$         d?     T! T3/ TB   R#   \&         d#   p\(        P+                  RY-4        Rp? R# Rp?ii ; i\&         d"   p\(        P+                  RY-4        Rp?R# Rp?ii ; i)a  Persist the routing peer for an existing gateway session row. ``transport_profile`` is the
entry's persisted receiving-bot profile; when the caller has no entry it is read off the
source's pinned identity (None = unknown, the column keeps whatever an earlier writer set).NÚrecord_gateway_session_peer)Úapi_storage_source)Ústorage_sourcerˆ   )r   r›   r2   rœ   rm   r�   rŽ   rl   rß   r�   z-Gateway session peer record failed for %s: %s)rŸ   r'   r¦   Úgateway.session_apirâ   Úgateway.session_a2arã   r)   r‰   rr   rX   r�   r›   rœ   rm   r�   r‘   r   ro   r   r§   r¨   )r-   r‹   r2   r   rŽ   rß   r�   r©   Úrecorderrâ   rã   r‰   r£   r«   s   &&&&&&&       r   rÀ   Ú1SessionRecoveryMixin._record_gateway_session_peerA  sl  € ð ×Ñ˜kÓ*ˆßŸÙÜ˜2Ð<¸dÓCˆÜ˜×!Ò!ÙÝ:Ý6ÝAÜÙ! "¨jÐ:LÈRÐ]c×]lÑ]l×]rÑ]rÓ:sÓtð  E÷  Mñ  Mð  [fØ—N‘N¨f×.>Ñ.>È&×JZÑJZô\ˆð	\ÙØñUØ"ñUØ1=×1QÐ1QÀ×AQÑAQðUä(¨Ó0ðUð /LðUð #4×"SÐ"SÑ7KÈFÓ7Sõ	Uøô
 ô 	`ð`Ù˜Ñ, tÕ,øÜô `Ü—‘ÐLÈk×_Ó_ûð`úäô 	\Ü�L‰LÐHÈ+×[Ò[ûð	\úsH   Â23C4 Ã&C4 Ã4E'Ä 	DÄD9ÄD4Ä-E'Ä4D9Ä9E'ÅE'ÅE"Å"E'c               ó$   € V ^8„  d   QhRRRRRR/# )r   r   r   r2   r9   r   rÎ   r   )r   s   "r   r   r    b  s'   € ÷ ^ñ ^°ð ^ÈCð ^ÐTXñ ^r   c                óø  € V P                  V4      pV'       g   R# RpV P                  ;_uu_ 4        V P                  4        V P                  P	                  V4      pW P                  9  d´   Ve°   \        VP                  RR4      pVe   WaP                  8H  pMVP                  R8H  pV'       do   V P                  V4      '       dX   V P                  P                  V4      pW$n        Wn        VP                  Vn        VP                  Vn        W@P                  V&   RRR4       Ve;   V P                  4        V P                  VP                  W!VP                   R7       R# R#   + '       g   i     LP; i)ai  One-time migration of pre-workspace-scope Slack keys: MOVE (not copy) the legacy entry so
a second workspace with identical Slack ids cannot attach to the same transcript. Adopt when
the legacy origin names the same workspace; a scope-less DM is claimed once by the first
workspace; a scope-less channel/group is refused (channel ids collide across workspaces).NrV   rk   r½   )rZ   Ú_lockÚ_ensure_loaded_lockedÚ_entriesrE   r'   rs   rV   rm   rg   Úpopr2   rX   Ú_save_entriesrÀ   r‹   rŽ   )r-   r   r2   r]   ÚmigratedÚlegacy_entryÚorigin_scopeÚadopts   &&&     r   Ú_adopt_legacy_slack_entryÚ.SessionRecoveryMixin._adopt_legacy_slack_entryb  s2  € ð
 ×3Ñ3°FÓ;ˆ
ßÙØ+/ˆØ�Z�Z‹ZØ×&Ñ&Ô(ØŸ=™=×,Ñ,¨ZÓ8ˆLØ§-¡-Ô/°LÒ4LÜ& |×':Ñ':¸JÈÓM�ØÒ+Ø(¯O©OÑ;‘Eà"×,Ñ,°Ñ4�Eß˜T×9Ñ9¸*×EÒEØ#Ÿ}™}×0Ñ0°Ó<�HØ+6Ô(Ø&,”OØ(.¯©�HÔ%Ø)/×)9Ñ)9�HÔ&Ø19—M‘M +Ñ.÷ ð  ÒØ×ÑÔ Ø×-Ñ-Ø×#Ñ# [Àx×G\ÑG\ð .ö ^ñ  ÷! �Zús   ²B E)Â3E)Ã
AE)Å)E9	Úduringc               ó8   € V ^8„  d   QhRRRRRRRRRR	R
RRRRR/# )r   r2   r9   Úend_session_idr   Ú
end_reasonÚcreate_kwargsr™   rs   r   rŽ   rõ   r   rÎ   r   )r   s   "r   r   r    €  sR   € ÷ ñ ØðØ3@ðØNQðà/ðà9Pðð $ðð .1ðð ;?ñr   c          	     ó*  aaaa€ S'       d   ^ RI Hp V! S4       V P                  S4      '       d$   S'       d   V P                  SSVVVV3R lR7       V P                  S4      '       d(   S'       d   V P	                  SSWVVVV3R lR7       R# R# R# )aM  SQLite side of a routing transition, outside ``_lock``: promote the predecessor row to an
explicit reset boundary (with the specific reason so state.db is auditable, e.g.
``suspended`` vs plain ``session_reset``), then INSERT the new row + routing
peer. Both best-effort: failures are warned and self-healed by the next peer refresh.)Úclose_session_runc                ó6   <€ \         P                  R SSSV 4      # )u–   Failed to end predecessor session row %s for %s%s: %s â€” the old row remains open and may win restart recovery until the next successful peer refresh)r§   rÊ   )Úerõ   r÷   r2   s   &€€€r   Ú<lambda>Ú?SessionRecoveryMixin._finish_route_transition.<locals>.<lambda>�  s   ø€ œfŸn™nð_à" K°¸ô<r   )rÔ   c                óT   <€ \         P                  R SP                  R4      SSV 4      # )ul   Failed to create session row %s for %s%s: %s â€” deferring to the self-healing peer refresh on the next turnr‹   )r§   rÊ   rE   )rý   rù   rõ   r2   s   &€€€r   rþ   rÿ   —  s)   ø€ œfŸn™nðAà!×%Ñ% lÓ3°[À&È!ôMr   N)Ú-hermes_cli.observability.relay_shared_metricsrû   rŸ   r×   Ú_create_session_row)	r-   r2   r÷   rø   rù   rs   rŽ   rõ   rû   s	   &fd$d$$d r   Ú_finish_route_transitionÚ-SessionRecoveryMixin._finish_route_transition€  s‚   û€ ÷ ÝWá˜nÔ-Ø×Ñ˜K×(Ò(¯^Ø×'Ñ'Ø˜^¨Zö<ð (ô ð ×Ñ˜K×(Ò(¯]Ø×$Ñ$Ø˜]¨FöMð %ö ñ .;Ñ(r   c               ó   € V ^8„  d   QhRR/# )r   r   rA   r   )r   s   "r   r   r    ž  s   € ÷ 
ñ 
à	ñ
r   c                óF  € ^ RI Hp RT RTRV'       d   VP                  MRRTRV'       d   VP                  MRRV'       d   VP                  MRR	V'       d   VP
                  MRR
V'       d   VP                  MRRV! V4      R\        V4      RTRTRV'       d   RV//# R/# )z·kwargs for ``SessionDB.create_session``. Identity (origin_json) and lineage
(parent/_reset_from) land atomically in the INSERT so a crash right after cannot strand the
row unroutable.rˆ   r‹   r   r›   Nr2   rœ   rm   r�   Úprofile_namer�   rl   rŽ   Úparent_session_idÚmodel_configÚ_reset_from)r)   r‰   r›   rœ   rm   r�   r+   r   )r‹   r2   rs   Úsource_valuerŽ   r  r‰   s   $$$$$$ r   Ú_session_create_kwargsÚ+SessionRecoveryMixin._session_create_kwargs�  s¥   € õ 	Bà˜*Ø�lØ¯�v—~’~°TØ˜;Ø¯�v—~’~°TØ¯V˜×)Ò)¸Ø¯V˜×)Ò)¸Ø¯f˜FŸNšN¸$ØÑ!5°fÓ!=Øœ<¨Ó/Ø˜LØÐ!2Ø×BS˜]Ð,=Ð>ð
ð 	
ð Z^ð
ð 	
r   c               ó   € V ^8„  d   QhRR/# )r   r   rÎ   r   )r   s   "r   r   r    µ  s   € ÷ 	ñ 	Ðbfñ 	r   c               óÂ   €  V P                  V4      P                  ! R/ VB  V P                  VR,          WVR7       R#   \         d   pT! T4        Rp?R# Rp?ii ; i)zÒINSERT a session row and record its routing peer; ``log(exc)`` on failure. A failed
create is a routing hazard (visible warning), but the row is self-healed with full identity
by the next per-turn peer refresh.r‹   r½   Nr   )rŸ   Úcreate_sessionrÀ   r   )r-   r2   Údb_create_kwargsrs   rŽ   rÔ   rý   s   &&&&&$ r   r  Ú(SessionRecoveryMixin._create_session_rowµ  s^   € ð	Ø×Ñ˜[Ó)×8Ò8ÑLÐ;KÒLØ×-Ñ-Ø  Õ.°ÐR^ð .ö `øäô 	Ù��FŠFûð	ús   ‚<A  Á AÁAÁAr   r}   )NFN)Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__Ú__doc__r/   Ústaticmethodr6   r<   rI   rR   rZ   rg   rt   r”   r    rž   r¹   rÃ   rÆ   r¾   r×   r¿   rÀ   ró   r  r  r  Ú__static_attributes__r   r   r   r   r   !   s
  † ÙW÷ð$ ô5ó ð5ð
 ôó ðõ@÷.;õaõ	ð ôQó ðQõ cð4Uà&+÷Uð ðÐV[ö ó ðð '"Ø@D÷'"ðRà&+÷ò$	ð&ØAF÷&õ<ð(^ÐZ\÷ ^÷\õB^ð<ð 46÷ð: ô
ó ð
÷.	ñ 	r   r   )r  Ú
__future__r   Úloggingr   rµ   rb   Údataclassesr   r   Úgateway.configr   Útypingr   r   r   r	   rQ   r
   r   Ú	getLoggerr§   r   r   r   r   r   Ú<module>r      sU   ðñ^õ #ã Û Û Û Ý Ý Ý #ß 5Ó 5çß;ð 
×	Ò	Ð,Ó	-€õ÷]ó ]r   