Ë
    âß²j;^  ã                  óÄ   — d Z ddlmZ ddlZddlZddlZddlZddlmZ ddl	m	Z	 ddl
mZ ddlmZmZmZmZ erddlmZmZ  ej(                  d	«      Zdd
„Z G d„ d«      Zy)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                ón   — | €y	 t        j                  | j                  «       «      S # t        $ r Y yw xY w)zA``source.to_dict()`` as JSON, or None when absent/unserializable.N)ÚjsonÚdumpsÚto_dictÚ	Exception)Úsources    ú=/home/ubuntu/.hermes/hermes-agent/gateway/session_recovery.pyÚ_origin_jsonr      s7   € à€~ØðÜ�z‰z˜&Ÿ.™.Ó*Ó+Ð+øÜò Ùðús   …"( ¨	4³4c                  óÂ  — e Zd ZdZdd d„Zed!d„«       Zed"d„«       Z	 	 	 	 	 	 d#d„Zdd$d„Z	d%d„Z
d&d	„Ze	 	 	 	 	 	 d'd
„«       Z	 	 	 	 	 	 	 	 	 	 d(d„Zddœ	 	 	 	 	 	 	 	 	 d)d„Zeddœ	 	 	 d*d„«       Zddœ	 	 	 	 	 d+d„Zddœ	 	 	 	 	 	 	 	 	 d,d„Zd„ Zddœ	 d-d„Zd.d„Zddœd/d„Z	 	 d0	 	 	 	 	 	 	 	 	 	 	 	 	 d1d„Zd2d„Zddœ	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d3d„Ze	 	 d4d„«       Zd5d„Zy)6ÚSessionRecoveryMixinzMSessionStore durable-row recovery and the SQLite side of routing transitions.Nc                óà   — t        | j                  dd«      syddlm}  ||«      }|�|j                  S |�|j
                  r|j
                  S 	 ddlm}  |«       xs dS # t        $ r Y yw xY w)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_profilesFNr   )Ú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_keyz-SessionRecoveryMixin._resolve_profile_for_key$   su   € ô �t—{‘{Ð$8¸%Ô@ØÝ8Ù˜vÓ&ˆØÐØ×/Ñ/Ð/ØÐ &§.¢.Ø—>‘>Ð!ð	ÝCÙ*Ó,Ò9°	Ð9øÜò 	Ùð	ús   ÁA! Á!	A-Á,A-c                ó”   — | syt        | «      j                  d«      }t        |«      dk  s|d   dk7  ryddlm}  ||d   xs d«      S )	z?Extract the profile namespace encoded in a gateway session key.Nú:é   r   Úagent)Ú"profile_from_session_key_namespaceé   Úmain)ÚstrÚsplitÚlenÚgateway.sessionr*   )Úsession_keyÚpartsr*   s      r   Ú_profile_from_session_keyz.SessionRecoveryMixin._profile_from_session_key6   sO   € ñ ØÜ�KÓ ×&Ñ& sÓ+ˆÜˆu‹:˜Š>˜U 1™X¨Ò0ØÝFÙ1°%¸±(Ò2D¸fÓEÐEó    c                 óD   — 	 ddl m}   | «       xs dS # t        $ r Y yw xY w)Nr   r   r   )r"   r   r   r   s    r   Ú_active_profile_namez)SessionRecoveryMixin._active_profile_nameA   s*   € ð	ÝCÙ*Ó,Ò9°	Ð9øÜò 	Ùð	ús   ‚ “	žc               ó  — t        |j                  d«      xs d«      }|r||k(  ry| j                  |«      }|€yt        | j                  dd«      r| j                  |«      }|du xs ||k(  S || j                  «       k(  S )u‚  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).
        r1   Ú TNr   F)r-   Úgetr3   r   r   r6   )r#   Úrequested_session_keyÚ	recoveredÚrecovered_keyÚrecovered_profileÚrequested_profiles         r   Ú)_recovered_row_allowed_for_active_profilez>SessionRecoveryMixin._recovered_row_allowed_for_active_profileI   s’   € ô ˜IŸM™M¨-Ó8Ò>¸BÓ?ˆÙ Ð1FÒ FØØ ×:Ñ:¸=ÓIÐØÐ$ØÜ�4—;‘;Ð 4°eÔ<Ø $× >Ñ >Ð?TÓ UÐØ$¨Ð,ÒVÐ0AÐEVÑ0VÐVØ  D×$=Ñ$=Ó$?Ñ?Ð?r4   c           	     ó    — ddl m}  ||�|n|t        | j                  dd«      t        | j                  dd«      | j	                  |«      ¬«      S )zQSession key for *source* (profile from *source*; key from *key_source* if given).r   )Úbuild_session_keyÚgroup_sessions_per_userTÚthread_sessions_per_userF)rB   rC   r!   )r0   rA   r   r   r%   )r#   r   Ú
key_sourcerA   s       r   Ú_generate_session_keyz*SessionRecoveryMixin._generate_session_key`   sN   € å5Ù Ø$Ð0‰J°fÜ$+¨D¯K©KÐ9RÐTXÓ$YÜ%,¨T¯[©[Ð:TÐV[Ó%\Ø×1Ñ1°&Ó9ô	;ð 	;r4   c                ó’   — |j                   t        j                  k7  s|j                  sy| j	                  |t        |dd¬«      «      S )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   ÚSLACKrG   rE   r   )r#   r   s     r   Ú_legacy_slack_session_keyz.SessionRecoveryMixin._legacy_slack_session_keyi   s:   € ð �?‰?œhŸn™nÒ,°F·O²OØØ×)Ñ)¨&´'¸&È4ÐZ^Ô2_Ó`Ð`r4   c                óà   — |sy| j                  dt        j                  «      5  | j                  dt        «      }||v r
	 ddd«       y|j	                  |«       	 ddd«       y# 1 sw Y   yxY w)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#   Ú
legacy_keyÚclaimeds      r   Ú_claim_legacy_slack_keyz,SessionRecoveryMixin._claim_legacy_slack_keyp   sg   € áØØ�Z‰ZÐ2´I·N±NÓCñ 	Ø—j‘jÐ!=¼sÓCˆGØ˜WÑ$Ø÷	ð 	ð �K‰K˜
Ô#Ø÷	÷ 	ñ 	ús   ¤A$Á	A$Á$A-c                ón  — |j                   t        j                  k7  s|j                  dk(  s|j                  sy	 t        j                  | j                  d«      xs d«      }t        |t        «      sy|j                  d|j                  d«      «      |j                  k(  S # t        t        f$ r Y yw xY w)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_jsonr8   FrG   rH   )rI   r   rJ   Ú	chat_typerG   r   Úloadsr9   Ú	TypeErrorÚ
ValueErrorÚ
isinstanceÚdict)r;   r   Úorigins      r   Ú#_recovered_row_matches_source_scopez8SessionRecoveryMixin._recovered_row_matches_source_scope{   s™   € ð �?‰?œhŸn™nÒ,°×0@Ñ0@ÀDÒ0HÐPV×P_ÒP_Øð	Ü—Z‘Z 	§¡¨mÓ <Ò BÀÓCˆFô ˜&¤$Ô'ØØ�z‰z˜* f§j¡j°Ó&<Ó=ÀÇÁÑPÐPøô	 œ:Ð&ò 	Ùð	ús   »(B" Â"B4Â3B4c               ó¸  — ddl m} dd„} ||j                  d«      t        j                  d«      «      }|j                  d«      }|�	 |||«      n|}	|j                  d«      }
|
€$t        |j                  d«      xs d«      xs |d u}
ddlm}  ||t        |d	   «      ||	||j                  |j                  |j                  t        |
«       ||«      ¬
«
      S )Nr   )r   c                óx   — 	 t        j                  t        | «      «      S # t        t        t
        f$ r |cY S w xY w©N)r   ÚfromtimestampÚfloatr\   r]   ÚOSError)Úvaluer   s     r   Ú_tszBSessionRecoveryMixin._create_entry_from_recovered_row.<locals>._ts‘   s7   € ðÜ×-Ñ-¬e°E«lÓ;Ð;øÜœz¬7Ð3ò Ø’ðús   ‚   9¸9Ú
started_atÚlast_activity_atÚ_has_messagesÚmessage_count©Útransport_profile_ofÚid)
r1   Ú
session_idÚ
created_atÚ
updated_atr`   Údisplay_namerI   rZ   Úreset_had_activityÚtransport_profile)r   r   Úreturnr   )r0   r   r9   r   re   Úboolr   ro   r-   Ú	chat_namerI   rZ   )r#   Úrowr1   r   Únowr   ri   rr   Úlast_activityrs   Úhad_activityro   s               r   Ú _create_entry_from_recovered_rowz5SessionRecoveryMixin._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r4   F)Úraise_on_lookup_errorc          
     óÚ   — | j                  | j                  |«      |j                  j                  ||j                  |r|j
                  nd|r|j                  nd|j                  |¬«      S )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   r1   Úuser_idÚchat_idrZ   Ú	thread_idr   )Ú	_peer_rowÚ_db_for_keyrI   rh   r�   r‚   rZ   rƒ   )r#   r1   r   Úallow_peer_fallbackr   s        r   Ú_find_gateway_session_rowz.SessionRecoveryMixin._find_gateway_session_row¦   sd   € ð �~‰~Ø×Ñ˜[Ó)°&·/±/×2GÑ2GÐU`Ø—N‘NÙ&9�F—N’N¸tÙ*=�f×&Ò&À4Ø×&Ñ&Ð>Sð ó Uð 	Ur4   c               ó´   — | rt        | dd«      nd}t        |«      sy	  |d||dœ|¤ŽS # t        $ r$}t        j	                  d||«       |r‚ Y d}~yd}~ww xY w)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_peerN©r   r1   z-Gateway session DB recovery failed for %s: %s© )r   Úcallabler   ÚloggerÚdebug)Údbr   r1   r   ÚpeerÚfinderÚexcs          r   r„   zSessionRecoveryMixin._peer_row³   sj   € ñ OQ”˜ÐCÀTÔJÐVZˆÜ˜ÔØð	ÙÐI °[ÑIÀDÑIÐIøÜò 	Ü�L‰LÐHÈ+ÐWZÔ[Ù$ØÜûð		ús   Ÿ
* ª	A³AÁA)Ú	not_afterc               óÞ  — |sy| j                  |«      }|€y| j                  |«      }|r||fS t        |«      j                  d«      }t	        |«      dk\  r|d   dk(  r|d   nd}|sy| j                  |||¬«      }t        |t        «      r|j                  d«      sy|j                  d	«      }|�-|�+t        j                  t        |«      «      t        |«      kD  ryt        |d   «      |fS )
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).
        Nr'   é   r   r)   r(   rŠ   rp   rj   )r…   Úpeek_session_idr-   r.   r/   r„   r^   r_   r9   ÚmathÚfloorrf   Úint)	r#   r1   r“   r�   rq   r2   rI   rz   rj   s	            r   Úresolve_session_id_for_keyz/SessionRecoveryMixin.resolve_session_id_for_keyÄ   sõ   € ñ  ØØ×Ñ˜kÓ*ˆØˆ:ØØ×)Ñ)¨+Ó6ˆ
ÙØ˜r�>Ð!Ü�KÓ ×&Ñ& sÓ+ˆÜ" 5›z¨Qš°5¸±8¸wÒ3F�5˜’8ÈDˆÙØð �n‰n˜R¨¸kˆnÓJˆÜ˜#œtÔ$¨C¯G©G°D¬MØØ—W‘W˜\Ó*ˆ
ð Ð  ZÐ%;ÄÇ
Á
Ì5ÐQ[ÓK\Ó@]Ô`cÐdmÓ`nÒ@nØÜ�3�t‘9‹~˜rÐ!Ð!r4   c               óÊ   — | j                  ||||¬«      \  }}|€y| j                  ||j                  «       |r)| j                  |j                  |||j                  ¬«       |S )zERebuild a missing session-key mapping from a recoverable durable row.)r1   r   r{   r   N©rt   )Ú_query_recoverable_rowÚ_reopen_session_rowrq   Ú_record_gateway_session_peerrt   )r#   r1   r   r{   r   ÚentryÚmigrated_legacys          r   Ú_recover_session_from_dbz-SessionRecoveryMixin._recover_session_from_dbí   s{   € ð "&×!<Ñ!<ð $¨F¸Ø"7ð "=ó "9Ñˆˆð ˆ=ØØ× Ñ  ¨e×.>Ñ.>Ô?ÙØ×-Ñ-Ø× Ñ  +¨vÀE×DVÑDVð .ô Xàˆr4   c               óŽ   — | j                  |||¬«      \  }}|�+|r)| j                  |j                  |||j                  ¬«       |S )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).)r1   r   r{   rœ   )r�   rŸ   rq   rt   )r#   r1   r   r{   r    r¡   s         r   Ú_query_recoverable_sessionz/SessionRecoveryMixin._query_recoverable_sessionÿ   s[   € ð "&×!<Ñ!<Ø#¨F¸ð "=ó "=ÑˆˆàÐ¡Ø×-Ñ-Ø× Ñ  +¨vÀE×DVÑDVð .ô Xàˆr4   c               ó®  — | j                  |«      }| j                  |||du |¬«      }d}|s3|r1| j                  |«      r | j                  ||d|¬«      }t        |«      }t	        |t
        «      sy| j                  ||«      sy| j                  ||¬«      s't        j                  d|j                  d«      |«       y| j                  ||||¬«      }||fS )	a
  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)r1   r   r†   r   F)NF)r:   r;   z\Gateway session DB recovery ignored %s for %s because the row belongs to a different profiler1   )rz   r1   r   r{   )rK   r‡   rV   rx   r^   r_   ra   r?   r�   Úwarningr9   r~   )	r#   r1   r   r{   r   rT   r;   r¡   r    s	            r   r�   z+SessionRecoveryMixin._query_recoverable_row
  s  € ð ×3Ñ3°FÓ;ˆ
Ø×2Ñ2Ø#¨FÈ
ÐVZÐHZØ"7ð 3ó 9ˆ	ð  ˆÙ™Z¨D×,HÑ,HÈÔ,TØ×6Ñ6Ø&¨vÈ5Ø&;ð 7ó =ˆIô # 9›oˆOÜ˜)¤TÔ*ØØ×7Ñ7¸	À6ÔJØØ×=Ñ=Ø"-¸ð >ô Dä�N‰Nð$Ø%.§]¡]°=Ó%AÀ;ôPð Ø×5Ñ5Ø {¸6Àsð 6ó Lˆà�oÐ%Ð%r4   c               ó  — 	 | j                  |«      }t        |dd«      }t        |«      r
 |||«       n|j                  ||«       d|› �}|j	                  |«      r|j                  |d«       yy# t        $ r} ||«       Y d}~yd}~ww xY w)al  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:r8   )r…   r   rŒ   Úend_sessionÚget_metaÚset_metar   )	r#   r1   rq   ÚreasonÚlogr�   ÚpromoteÚheartbeat_keyr’   s	            r   Ú_promote_session_resetz+SessionRecoveryMixin._promote_session_reset(  sŒ   € ð
	Ø×!Ñ! +Ó.ˆBÜ˜bÐ"<¸dÓCˆGÜ˜Ô Ù˜
 FÕ+à—‘˜z¨6Ô2ð )¨¨Ð5ˆMØ�{‰{˜=Ô)Ø—‘˜M¨2Õ.ð *øäò 	Ù��H‰Hûð	ús   ‚A-A1 Á1	BÁ:BÂBr8   )Ú
log_prefixc               óÞ   — 	 | j                  |«      j                  |«       y# t        $ r@}|rt        j	                  d||«       nt        j	                  d||«       Y d}~yY d}~yd}~ww xY w)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#   r1   rq   r±   r’   s        r   rž   z(SessionRecoveryMixin._reopen_session_row<  s`   € ð	^Ø×Ñ˜[Ó)×8Ñ8¸ÕDøÜò 	^ÙÜ—‘˜X z°3Õ7ä—‘ÐJÈKÐY\×]Ñ]ô 8ûð	^ús   ‚ # £	A,¬1A'Á'A,c           
     óJ  — | j                  |«      }|r|syt        |dd«      }t        |«      syddlm}	 t        |j                  j                  |j                  ||j                  |j                  |j                  ¬«      }
	  ||fi |
¤|xs |j                  t        |«      ||xs  |	|«      dœ¤Ž y# t        $ r; 	  ||fi |
¤Ž Y y# t        $ r"}t         j#                  d||«       Y d}~Y yd}~ww xY wt        $ r!}t         j#                  d||«       Y d}~yd}~ww xY w)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_peerr   rn   )r   r�   r1   r‚   rZ   rƒ   )rt   rY   Úinclude_compression_ancestorsrv   z-Gateway session peer record failed for %s: %s)r…   r   rŒ   r   ro   r_   rI   rh   r�   r‚   rZ   rƒ   ry   r   r\   r   r�   rŽ   )r#   rq   r1   r   rt   r¶   rv   r�   Úrecorderro   r�   r’   s               r   rŸ   z1SessionRecoveryMixin._record_gateway_session_peerF  s$  € ð ×Ñ˜kÓ*ˆÙ™ØÜ˜2Ð<¸dÓCˆÜ˜Ô!ØÝAÜØ—?‘?×(Ñ(°&·.±.ÈkØ—N‘N¨f×.>Ñ.>È&×JZÑJZô\ˆð	\ÙØñUØ"ðUØ1=Ò1QÀ×AQÑAQÜ(¨Ó0Ø.KØ"3Ò"SÑ7KÈFÓ7Sõ	Uøô
 ò 	`ð`Ù˜Ñ, tÔ,øÜò `Ü—‘ÐLÈkÐ[^×_Ò_ûð`úäò 	\Ü�L‰LÐHÈ+ÐWZ×[Ñ[ûð	\ús<   Â1B6 Â6	D"Ã 	CÃ	C6ÃC1Ã+D"Ã1C6Ã6D"ÄDÄD"c                óª  — | j                  |«      }|syd}| j                  5  | j                  «        | j                  j	                  |«      }|| j                  vr§|�¥t        |j                  dd«      }|�||j                  k(  }n|j                  dk(  }|rk| j                  |«      rZ| j                  j                  |«      }||_        ||_        |j                  |_        |j                  |_        || j                  |<   ddd«       |�:| j                  «        | j                  |j                  |||j                   ¬«       yy# 1 sw Y   ŒFxY w)a�  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).NrG   rX   rœ   )rK   Ú_lockÚ_ensure_loaded_lockedÚ_entriesr9   r   r`   rG   rZ   rV   Úpopr1   rI   Ú_save_entriesrŸ   rq   rt   )r#   r   r1   rT   ÚmigratedÚlegacy_entryÚorigin_scopeÚadopts           r   Ú_adopt_legacy_slack_entryz.SessionRecoveryMixin._adopt_legacy_slack_entrye  sA  € ð
 ×3Ñ3°FÓ;ˆ
ÙØØ+/ˆØ�Z‰Zñ 	:Ø×&Ñ&Ô(ØŸ=™=×,Ñ,¨ZÓ8ˆLØ $§-¡-Ñ/°LÐ4LÜ& |×':Ñ':¸JÈÓM�ØÐ+Ø(¨F¯O©OÑ;‘Eà"×,Ñ,°Ñ4�EÙ˜T×9Ñ9¸*ÔEØ#Ÿ}™}×0Ñ0°Ó<�HØ+6�HÔ(Ø&,�H”OØ(.¯©�HÔ%Ø)/×)9Ñ)9�HÔ&Ø19�D—M‘M +Ñ.÷	:ð  ÐØ×ÑÔ Ø×-Ñ-Ø×#Ñ# [°&Àx×G\ÑG\ð .õ ^ð  ÷!	:ð 	:ús   £C!E	Å	E)Úduringc          	     óÆ   ‡‡‡‡— | j                  ‰«      r‰r| j                  ‰‰|ˆˆˆfd„¬«       | j                  ‰«      r‰r| j                  ‰‰||ˆˆˆfd„¬«       yyy)ae  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.c                ó6   •— t         j                  d‰‰‰| «      S )Nu–   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Ã   Úend_session_idr1   s    €€€r   ú<lambda>z?SessionRecoveryMixin._finish_route_transition.<locals>.<lambda>Ž  s   ø€ œfŸn™nð_à" K°¸ó<€ r4   )r­   c                óT   •— t         j                  d‰j                  d«      ‰‰| «      S )Nul   Failed to create session row %s for %s%s: %s â€” deferring to the self-healing peer refresh on the next turnrq   )r�   r¦   r9   )rÆ   Úcreate_kwargsrÃ   r1   s    €€€r   rÈ   z?SessionRecoveryMixin._finish_route_transition.<locals>.<lambda>–  s+   ø€ œfŸn™nðAà!×%Ñ% lÓ3°[À&È!óM€ r4   N)r…   r°   Ú_create_session_row)r#   r1   rÇ   Ú
end_reasonrÊ   r`   rt   rÃ   s    `` `  `r   Ú_finish_route_transitionz-SessionRecoveryMixin._finish_route_transitionƒ  sp   û€ ð ×Ñ˜KÔ(©^Ø×'Ñ'Ø˜^¨Zõ<ð (ô ð ×Ñ˜KÔ(©]Ø×$Ñ$Ø˜]¨F°LõMð %õ ð .;Ð(r4   c                óè   — ddl m} | ||r|j                  nd||r|j                  nd|r|j                  nd|r|j
                  nd|r|j                  nd ||«      t        |«      |||rd|idœS ddœS )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   rn   NÚ_reset_from)rq   r   r�   r1   r‚   rZ   rƒ   Úprofile_namerv   rY   rt   Úparent_session_idÚmodel_config)r   ro   r�   r‚   rZ   rƒ   r!   r   )rq   r1   r`   Úsource_valuert   rÑ   ro   s          r   Ú_session_create_kwargsz+SessionRecoveryMixin._session_create_kwargsœ  sˆ   € õ 	Bà$Ø"Ù)/�v—~’~°TØ&Ù)/�v—~’~°TÙ-3˜×)Ò)¸Ù-3˜×)Ò)¸Ù.4˜FŸNšN¸$Ù!5°fÓ!=Ü'¨Ó/Ø(Ø!2ÙBS˜]Ð,=Ð>ñ
ð 	
ð Z^ñ
ð 	
r4   c               ó´   — 	  | j                  |«      j                  di |¤Ž | j                  |d   |||¬«       y# t        $ r} ||«       Y d}~yd}~ww xY w)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.rq   rœ   Nr‹   )r…   Úcreate_sessionrŸ   r   )r#   r1   Údb_create_kwargsr`   rt   r­   rÆ   s          r   rË   z(SessionRecoveryMixin._create_session_row´  sb   € ð	Ø8ˆD×Ñ˜[Ó)×8Ñ8ÑLÐ;KÒLØ×-Ñ-Ø  Ñ.°¸VÐR^ð .õ `øäò 	Ù��F‰Fûð	ús   ‚9< ¼	AÁAÁArd   )r   úOptional[SessionSource]rw   úOptional[str])r1   rÙ   rw   rÙ   )rw   r-   )r:   r-   r;   úDict[str, Any]rw   rx   )r   r   rD   rØ   rw   r-   )r   r   rw   rÙ   )rT   rÙ   rw   rx   )r;   rÚ   r   r   rw   rx   )
rz   rÚ   r1   r-   r   r   r{   r   rw   r   )
r1   r-   r   r   r†   rx   r   rx   rw   úOptional[Dict[str, Any]])
r   r-   r1   r-   r   rx   r�   r   rw   rÛ   )r1   r-   r“   zOptional[float]rw   zOptional[tuple[str, Any]])
r1   r-   r   r   r{   r   r   rx   rw   zOptional[SessionEntry])rw   z#tuple[Optional[SessionEntry], bool])r1   r-   rq   r-   r¬   r-   rw   ÚNone)r1   r-   rq   r-   r±   r-   rw   rÜ   )NFN)rq   r-   r1   r-   r   rØ   rt   rÙ   r¶   rx   rv   rÙ   rw   rÜ   )r   r   r1   r-   rw   rÜ   )r1   r-   rÇ   rÙ   rÌ   r-   rÊ   rÛ   r`   rØ   rt   rÙ   rÃ   r-   rw   rÜ   )rw   rÚ   )rw   rÜ   )Ú__name__Ú
__module__Ú__qualname__Ú__doc__r%   Ústaticmethodr3   r6   r?   rE   rK   rV   ra   r~   r‡   r„   rš   r¢   r¤   r�   r°   rž   rŸ   rÂ   rÍ   rÔ   rË   r‹   r4   r   r   r   !   su  „ ÙWôð$ òFó ðFð òó ðð@Ø(+ð@Ø8Fð@à	ó@ô.;óaó	ð ðQØ!ðQØ+8ðQà	òQó ðQð cØ$ðcØ36ðcØ@MðcØT\ðcà	ócð8 ',ñUØ!ðUØ+8ðUØOSðUà#ðUà0HóUð ØV[ñ ØðØ":òó ðð" AEñ'"Øð'"Ø.=ð'"à	"ó'"ðV ',ñØ!ðØ+8ðØ?Gðà#ðà0Fóò$	ð BGñ&à	,ó&ó<ð( []õ ^ð SXØ+/ð\Øð\Ø,/ð\Ø9Pð\à#ð\àKOð\ð )ð\ð 59ó\ó>^ðB 46ñØðØ3@ðØNQðà/ðà9Pðð $ðð .1ðð ;?óð2 ð
à	ò
ó ð
ô.	r4   r   )rw   rÙ   )rà   Ú
__future__r   Úloggingr   r—   rP   Údataclassesr   r   Úgateway.configr   Útypingr   r   r	   r
   r0   r   r   Ú	getLoggerr�   r   r   r‹   r4   r   ú<module>rè      sW   ðñ^õ #ã Û Û Û Ý Ý Ý #ß 5Ó 5áß;ð 
ˆ×	Ñ	Ð,Ó	-€ó÷\ò \r4   