Ë
    âß²j€l  ã                  óÆ   — d Z ddlmZ ddlZddlZddlZddlZddlmZ ddl	m
Z
mZmZmZ ddlmZ e
rddlmZ  ej$                  d«      Z e«       Zd	Zdd
„Z G d„ d«      Zy)a  SessionStore storage plumbing: per-profile SessionDB handle resolution and the routing-index
load/save paths (state.db gateway_routing primary, sessions.json legacy mirror). Mixin split out of
``gateway/session.py``; bound onto ``SessionStore`` via the MRO.é    )ÚannotationsN)ÚPath)ÚTYPE_CHECKINGÚAnyÚDictÚOptional)Úatomic_json_write©ÚSessionEntryzgateway.sessiona©  LEGACY MIRROR of the gateway routing index (the primary copy lives in the gateway_routing table in ~/.hermes/state.db). Maps messaging session keys (agent:main:<platform>:...) to active session IDs. This is NOT the session list. ALL sessions (CLI, TUI, and gateway) live in ~/.hermes/state.db and are shown by `hermes sessions list` and `/sessions`. Disable this file with `gateway.write_sessions_json: false` in config.yaml.c                ó@   — t        | t        «      xr dt        | «      v S )zCTest-isolation guard: must stay a loud failure and is never cached.zlive-system guard)Ú
isinstanceÚRuntimeErrorÚstr)Úexcs    ú@/home/ubuntu/.hermes/hermes-agent/gateway/session_persistence.pyÚ_is_live_system_guardr   #   s   € ä�cœ<Ó(ÒLÐ-@ÄCÈÃHÐ-LÐLó    c                  óN  — e Zd ZdZd#d$d„Zd„ Zed„ «       Zej                  d%d„«       Zed„ «       Z	d&d„Z
d'd	„Zd(d
„Zd)d„Zd*d„Zd%d„Zd%d„Zd+d„Zd,d„Zd-d„Zd.d„Zed/d„«       Zd%d„Zd0d„Zd%d„Zd1d„Zd2d„Zd%d„Zd3d„Zd%d„Zd4d„Zd5d„Z d6d„Z!d%d„Z"dd d!œ	 	 	 	 	 	 	 d7d"„Z#y)8ÚSessionPersistenceMixinzWSessionStore storage plumbing: SessionDB handle resolution and routing-index load/save.Nc                ó¨   ‡‡— ddl m} ddlmŠ |�t	        |«      nt	         |«       «      Šˆˆfd„}| j
                  j                  ‰|t        ¬«      S )a)  SessionDB for the active profile scope. ``db_path`` pins the store; otherwise
        ``_default_db_path()`` follows the context-local HERMES_HOME (resolved per call so
        multiplexed profiles reach their own store). Handles are cached per path; failed opens enter
        a bounded backoff during which callers keep using the JSONL fallback.

        Resolving here rather than once in ``__init__`` is the whole fix for #88532: it lets the scoping
        that the multiplexed inbound path already performs actually reach session storage.
        r   )Ú_default_db_path)Úacquirec                 ól   •— 	  ‰‰«      S # t         $ r} t        | «      st        d| › �«       ‚ d } ~ ww xY w)NzL[gateway] Warning: SQLite session store unavailable, falling back to JSONL: )Ú	Exceptionr   Úprint)Úer   Úpaths    €€r   Ú_openzHSessionPersistenceMixin._open_session_db_for_active_scope.<locals>._open9   s?   ø€ ðÙ˜t“}Ð$øÜò Ü,¨QÔ/ÜÐhÐijÐhkÐlÔmØûðús   ƒ ‹	3”.®3)Únon_cacheable)Úhermes_stater   Úhermes_state_registryr   r   Ú_db_handle_cacheÚgetr   )ÚselfÚdb_pathr   r   r   r   s       @@r   Ú!_open_session_db_for_active_scopez9SessionPersistenceMixin._open_session_db_for_active_scope+   sJ   ù€ õ 	2Ý1à 'Ð 3Œt�GŒ}¼Ñ>NÓ>PÓ9Qˆõ	ð ×$Ñ$×(Ñ(¨¨uÔDYÐ(ÓZÐZr   c                ó$   — t        | dt        «      S )zKReturn the explicitly pinned DB (``store._db = x``), else ``_DB_UNPINNED``.Ú
_db_pinned)ÚgetattrÚ_DB_UNPINNED©r$   s    r   Ú
_pinned_dbz"SessionPersistenceMixin._pinned_dbC   s   € ä�t˜\¬<Ó8Ð8r   c                óV   — | j                  «       }|t        u r| j                  «       S |S )a=  The SessionDB for the active profile scope, or a pinned override. Assigning ``store._db``
        pins that value for every subsequent read (tests install a fake or disable the DB with
        ``store._db = None``); unpinned, each read resolves the scope so a multiplexed profile's
        writes reach its own store.)r,   r*   r&   )r$   Úpinneds     r   Ú_dbzSessionPersistenceMixin._dbG   s,   € ð —‘Ó"ˆØ;AÄ\Ñ;Qˆt×5Ñ5Ó7Ð]ÐW]Ð]r   c                ó   — || _         y ©N)r(   )r$   Úvalues     r   r/   zSessionPersistenceMixin._dbP   s	   € àˆ�r   c                ó¶   — | j                  «       }|t        ur|S t        | dd«      }	 |€| j                  S | j	                  |dz  ¬«      S # t
        $ r Y yw xY w)uQ  The one store that owns the routing index, whatever scope is active. ``_entries`` is one
        flat dict holding every profile's keys, so it must persist to ONE file (``_routing_home``),
        not whichever profile is scoped â€” otherwise a mid-turn rewrite and the unscoped startup
        load see different copies and crash markers under a secondary profile go unrecovered. A
        pinned handle still wins; bare test instances lacking the handle cache report no DB.

        Reading it through ``_db`` made that file whichever profile happened to be scoped at the time: a
        whole-index rewrite during one profile's turn copied every other profile's routing rows into that
        profile's store, and startup â€” which runs unscoped â€” then loaded a different copy than the one the
        last writer produced. See #66887.
        Ú_routing_homeNústate.db©r%   )r,   r*   r)   r/   r&   r   )r$   r.   Úhomes      r   Ú_routing_dbz#SessionPersistenceMixin._routing_dbT   si   € ð —‘Ó"ˆØœÑ%ØˆMÜ�t˜_¨dÓ3ˆð	Øˆ|Ø—x‘x�Ø×9Ñ9À$ÈÑBSÐ9ÓTÐTøÜò 	Ùð	ús   ©A ·A Á	AÁAc                ój   — t        | j                  dd«      sy| j                  |«      }|r|dk(  rdS |S )u  The non-default profile that owns *session_key*, or None (ambient store is authoritative:
        multiplexing off, or legacy ``agent:main``). Deliberately does NOT cover "that profile has
        no directory" â€” ownership and resolvability are separate questions for ``_db_for_key``.Úmultiplex_profilesFNÚdefault)r)   ÚconfigÚ_profile_from_session_key)r$   Úsession_keyÚprofiles      r   Ú_named_profile_for_keyz.SessionPersistenceMixin._named_profile_for_keyl   s<   € ô �t—{‘{Ð$8¸%Ô@ØØ×0Ñ0°Ó=ˆÙ" g°Ò&:ˆtÐGÀÐGr   c                ó  — | j                  |«      }|€y| j                  }||v r||   S d}	 ddlm}m}  ||«      rt         ||«      «      }|�|||<   |S # t        $ r#}t        j                  d||«       d}Y d}~Œ0d}~ww xY w)zbHERMES_HOME of the profile owning *session_key*, or None (no named owner or
        unresolvable).Nr   )Úget_profile_dirÚprofile_existsz)Could not resolve profile home for %r: %s)	r@   Ú_profile_home_cacheÚhermes_cli.profilesrB   rC   r   r   ÚloggerÚdebug)r$   r>   r?   Úcacher7   rB   rC   r   s           r   Ú_profile_home_for_keyz-SessionPersistenceMixin._profile_home_for_keyu   sœ   € ð ×-Ñ-¨kÓ:ˆØˆ?ØØ×(Ñ(ˆØ�eÑØ˜‘>Ð!Ø#ˆð	ßKÙ˜gÔ&Ü™O¨GÓ4Ó5�ð ÐØ!ˆE�'‰NØˆøô ò 	Ü�L‰LÐDÀkÐSVÔWØ�Dûð	ús   ­!A Á	BÁ A>Á>Bc                óª  — | j                  «       }|t        ur|S | j                  |«      }|€Ht        | dd«      }|�t        | j                  dd«      s| j
                  S 	 | j                  |dz  ¬«      S | j                  |«      }|€t        j                  d||«       y	 | j                  |dz  ¬«      S # t        $ r Y yw xY w# t        $ r Y yw xY w)up  The SessionDB holding *session_key*'s rows, whatever scope is active (the owning profile
        is encoded in the key). ``_db`` follows the ambient HERMES_HOME that only the inbound message
        path installs; unscoped background work (expiry watcher) would otherwise write profile rows
        into the ROOT store until the stale-route self-heal drops a live conversation.

        Background work runs unscoped while operating on every profile's keys out of the single process-wide
        ``_entries`` dict â€” ``_session_housekeeping_watcher`` is the clearest case â€” so it reads and writes the
        ROOT store for rows that actually live under ``profiles/<name>/state.db``. The two writers then
        drift apart on the same logical session until the routing index disagrees with the row and the
        #54878 self-heal drops a live conversation (#66887).
        Nr4   r:   Fr5   r6   zggateway.session: profile %r has no resolvable home (key %r); refusing to fall back to the ambient store)r,   r*   r@   r)   r<   r/   r&   r   rI   rF   Úwarning)r$   r>   r.   r?   Úrouting_homer7   s         r   Ú_db_for_keyz#SessionPersistenceMixin._db_for_keyŒ   sû   € ð —‘Ó"ˆØœÑ%ØˆMØ×-Ñ-¨kÓ:ˆØˆ?ô # 4¨¸$Ó?ˆLØÐ#¬7°4·;±;Ð@TÐV[Ô+\Ø—x‘x�ðØ×=Ñ=ÀlÐU_ÑF_Ð=Ó`Ð`ð ×)Ñ)¨+Ó6ˆØˆ<ô �N‰Nð,Ø-4°kôCð ð	Ø×9Ñ9À$ÈÑBSÐ9ÓTÐTøô ò Ùðûô ò 	Ùð	ús$   Á!B7 Â"C Â7	CÃCÃ	CÃCc                óô   — |sy	 t        | j                  j                  «       «      D ]  }|j                  |k(  sŒ|j                  c S  	 t        | dd«      xs i j                  |«      S # t
        $ r Y Œ+w xY w)a
  The routing key that owns *session_id*, or None. The published index is authoritative;
        ``_session_owner_hints`` covers the window where ownership is proven but routing not yet
        published. Deliberately lock-free: several callers already hold ``_lock``.NÚ_session_owner_hints)ÚlistÚ_entriesÚvaluesÚ
session_idr>   r   r)   r#   )r$   rS   Úentrys      r   Ú_owner_key_for_session_idz1SessionPersistenceMixin._owner_key_for_session_id´   s€   € ñ Øð	Ü˜dŸm™m×2Ñ2Ó4Ó5ò -�Ø×#Ñ# zÓ1Ø ×,Ñ,Ò,ñ-ô
 ˜Ð4°dÓ;ÒA¸r×FÑFÀzÓRÐRøô ò 	Ùð	ús   …5A+ »A+ Á	A+ Á+	A7Á6A7c                ó^   — |s| j                   S | j                  | j                  |«      «      S )zŒThe SessionDB holding *session_id*'s row (owner from the index or a pre-published hint;
        unknown ids fall back to the ambient store).)r/   rM   rU   )r$   rS   s     r   Ú_db_for_session_idz*SessionPersistenceMixin._db_for_session_idÂ   s,   € ñ Ø—8‘8ˆOØ×Ñ × >Ñ >¸zÓ JÓKÐKr   c                óB   — dd„}| j                   j                  |«       y)a  Close every SessionDB handle this store opened (one per path). Closing only ``store._db``
        would strand secondary profiles' handles with their WAL lock held ('database is locked' on
        restart). Drained under the lock, closed outside it; a pinned handle is the pinner's.c                óz   — ddl m} 	  || «       y # t        $ r }t        j	                  d|«       Y d }~y d }~ww xY w)Nr   )Úrelease_or_closez-SessionDB close error during handle sweep: %s)r!   rZ   r   rF   rG   )ÚdbrZ   r   s      r   Ú_closez<SessionPersistenceMixin.close_all_db_handles.<locals>._closeÍ   s7   € Ý>ðSÙ  Õ$øÜò SÜ—‘ÐLÈc×RÑRûðSús   ˆ ‘	:š5µ:N©ÚreturnÚNone)r"   Ú	close_all)r$   r\   s     r   Úclose_all_db_handlesz,SessionPersistenceMixin.close_all_db_handlesÉ   s   € ó	Sð 	×Ñ×'Ñ'¨Õ/r   c                óf   — | j                   5  | j                  «        ddd«       y# 1 sw Y   yxY w)z4Load sessions index from disk if not already loaded.N)Ú_lockÚ_ensure_loaded_lockedr+   s    r   Ú_ensure_loadedz&SessionPersistenceMixin._ensure_loadedÖ   s*   € à�Z‰Zñ 	)Ø×&Ñ&Ô(÷	)÷ 	)ñ 	)ús   �'§0c                óX   — | j                  «        | j                  j                  |«      S )zALoad the index and return the entry for *session_key*. Lock held.)rd   rQ   r#   )r$   r>   s     r   Ú_entry_lockedz%SessionPersistenceMixin._entry_lockedÛ   s#   € à×"Ñ"Ô$Ø�}‰}× Ñ  Ó-Ð-r   c                ó¤   — 	 t        t        | j                  «      j                  «       «      S # t        $ r t        | j                  «      cY S w xY w)z†Namespace for this store's gateway_routing rows: the resolved sessions_dir, so stores
        with different dirs never share entries.)r   r   Úsessions_dirÚresolver   r+   s    r   Ú_routing_scopez&SessionPersistenceMixin._routing_scopeà   sF   € ð	*Ü”t˜D×-Ñ-Ó.×6Ñ6Ó8Ó9Ð9øÜò 	*Ü�t×(Ñ(Ó)Ò)ð	*ús   ‚+. ®AÁAc                óV   — t        | j                  xs d|d«      }t        |«      r|S dS )zHBound ``_routing_db.<name>`` if the handle exists and has it, else None.N)r)   r8   Úcallable)r$   ÚnameÚmethods      r   Ú_routing_db_methodz*SessionPersistenceMixin._routing_db_methodè   s-   € ä˜×)Ñ)Ò1¨T°4¸Ó>ˆÜ! &Ô)ˆvÐ3¨tÐ3r   c                ó(  — | j                  d«      }|€y	  || j                  «       ¬«      j                  «       D ])  \  }}| j                  ||«      }|€Œ|| j                  |<   Œ+ y# t
        $ r }t        j                  d|«       Y d}~yd}~ww xY w)z~Load state.db routing entries into ``_entries``; False when there is no loader or the
        load failed (warned). Lock held.Úload_gateway_routing_entriesNF©ÚscopeTz1gateway.session: state.db routing load failed: %s)rp   rk   ÚitemsÚ_routing_entry_from_jsonrQ   r   rF   rK   )r$   ÚloaderÚkeyÚ
entry_jsonrT   r   s         r   Ú_load_routing_rows_lockedz1SessionPersistenceMixin._load_routing_rows_lockedí   sš   € ð ×(Ñ(Ð)GÓHˆØˆ>Øð	Ù#)°×0CÑ0CÓ0EÔ#F×#LÑ#LÓ#Nò /‘��ZØ×5Ñ5°c¸:ÓF�ØÑ$Ø).�D—M‘M #Ò&ð/ð øÜò 	Ü�N‰NÐNÐPQÔRÜûð	ús   –?A( ÁA( Á(	BÁ1BÂBc                óò   — ddl m} 	 t        j                  |«      }t	        |t
        «      r |j                  |«      S 	 y# t        t        t        f$ r!}t        j                  d| |«       Y d}~yd}~ww xY w)zBParse one gateway_routing row; None (with a warning) when invalid.r   r
   z%Skipping invalid routing entry %r: %sN)Úgateway.sessionr   ÚjsonÚloadsr   ÚdictÚ	from_dictÚ
ValueErrorÚKeyErrorÚ	TypeErrorrF   rK   )rx   ry   r   Ú
entry_datar   s        r   rv   z0SessionPersistenceMixin._routing_entry_from_jsoný   st   € õ 	1ð	LÜŸ™ JÓ/ˆJÜ˜*¤dÔ+Ø-�|×-Ñ-¨jÓ9Ð9ð ,ð øô œH¤iÐ0ò 	LÜ�N‰NÐBÀCÈ×KÐKØûð	Lús   ˆ6A ÁA6ÁA1Á1A6c                óZ  — | j                   r| j                  «        y| j                  j                  dd¬«       | j	                  «       }|xr t        | j                  «      }| j                  |«       d| _         || _        |rdn| j                  «       | _
        | j                  «        y)aÖ  Load the routing index (lock held). state.db ``gateway_routing`` is primary;
        sessions.json is the legacy import for keys the DB lacks (persisted on the next _save).

        Read order (#9006 follow-up): the ``gateway_routing`` table in state.db is the primary source;
        sessions.json is the legacy import path for pre-migration installs (its entries are folded in for
        keys the DB doesn't have, then persisted to the DB on the next _save).
        NT)ÚparentsÚexist_ok)Ú_loadedÚ#_reconcile_recovered_routing_lockedri   Úmkdirrz   ÚboolrQ   Ú_import_legacy_sessions_jsonÚ_routing_db_loadedÚ_entries_as_dictsÚ_routing_fallback_baselineÚ_prune_stale_sessions_locked)r$   Údb_load_succeededÚdb_had_entriess      r   rd   z-SessionPersistenceMixin._ensure_loaded_locked	  s“   € ð �<Š<Ø×4Ñ4Ô6ØØ×Ñ×Ñ¨°tÐÔ<Ø ×:Ñ:Ó<ÐØ*ÒB¬t°D·M±MÓ/BˆØ×)Ñ)¨.Ô9ØˆŒØ"3ˆÔÙ2C©$È×I_ÑI_ÓIaˆÔ'à×)Ñ)Õ+r   c                óö  — ddl m} | j                  dz  }|j                  «       sy	 t	        |dd¬«      5 }t        j                  |«      }ddd«       d}j                  «       D ]…  \  }}|j                  d«      s|| j                  v rŒ&t        |t        «      s+t        j                  d	|t        |«      j                  «       Œa	  |j                   |«      | j                  |<   |d
z  }Œ‡ |r"|rt        j)                  d||d
k(  rdnd«       yyy# 1 sw Y   ŒÈxY w# t"        t$        t&        f$ r!}	t        j                  d||	«       Y d}	~	Œéd}	~	ww xY w# t*        $ r}	t-        d|	› �«       Y d}	~	yd}	~	ww xY w)zELegacy import: sessions.json fills only keys the DB lacks. Lock held.r   r
   úsessions.jsonNÚrzutf-8)ÚencodingÚ_z8Skipping invalid session entry %r: expected dict, got %sé   z%Skipping invalid session entry %r: %sz\gateway.session: imported %d legacy sessions.json entr%s missing from state.db routing tableÚyÚiesz,[gateway] Warning: Failed to load sessions: )r|   r   ri   ÚexistsÚopenr}   Úloadru   Ú
startswithrQ   r   r   rF   rK   ÚtypeÚ__name__r€   r�   r‚   rƒ   Úinfor   r   )
r$   r’   r   Úsessions_fileÚfÚdataÚimportedrx   r„   r   s
             r   rŒ   z4SessionPersistenceMixin._import_legacy_sessions_json  sp  € å0Ø×)Ñ)¨OÑ;ˆØ×#Ñ#Ô%Øð	FÜ�m S°7Ô;ð $¸qÜ—y‘y “|�÷$àˆHØ#'§:¡:£<ò T‘��Zà—>‘> #Ô&¨#°·±Ñ*>ØÜ! *¬dÔ3Ü—N‘NØRÐTWÜ˜ZÓ(×1Ñ1ô3ð ðTØ)?¨×)?Ñ)?À
Ó)K�D—M‘M #Ñ&Ø ‘M‘HðTñ ™NÜ—‘ð-Ø.6¸xÈ1º}¹ÐRWõYð +ˆx÷#$ð $ûô #¤H¬iÐ8ò TÜ—N‘NÐ#JÈCÐQR×SÑSûðTûô ò 	FÜÐ@ÀÀÐD×EÑEûð	FúsY   ¨E ¶DÁA;E Ã$DÃ,$E ÄDÄE ÄEÄ3EÅ
E ÅEÅE Å	E8Å E3Å3E8c                ó   — | j                   syg }d}	 | j                   j                  «       D ]‰  \  }}| j                  |«      }|€Œ|j                  |j                  «      }|�|j                  d«      €ŒI| j                  |||«      }|dk(  r|j                  |«       Œs|€Œv|| j                   |<   |dz  }Œ‹ 	 |D ]  }| j                   |= Œ |s|r| j                  «        yy# t        $ r }t        j                  d|«       Y d}~yd}~ww xY w)záRemove routing entries whose session has ended in state.db (startup, lock held). Stale ==
        ``end_reason IS NOT NULL``; rows absent from the DB are kept; a ``None`` DB handle is a
        no-op; DB errors are non-fatal.Nr   Ú
end_reasonÚpruner˜   z@gateway.session: stale-entry pruning skipped due to DB error: %s)rQ   ru   rM   Úget_sessionrS   r#   Ú_stale_entry_verdictÚappendr   rF   rK   Ú_save)	r$   Ú
stale_keysÚrecovered_keysrx   rT   r[   ÚrowÚverdictr   s	            r   r�   z4SessionPersistenceMixin._prune_stale_sessions_locked=  s  € ð �}Š}ØØˆ
Øˆð	Ø"Ÿm™m×1Ñ1Ó3ò (‘
��Uð ×%Ñ% cÓ*�Ø�:ØØ—n‘n U×%5Ñ%5Ó6�Ø�; #§'¡'¨,Ó"7Ð"?ØØ×3Ñ3°C¸ÀÓD�Ø˜gÒ%Ø×%Ñ% cÕ*ØÑ(Ø)0�D—M‘M #Ñ&Ø" aÑ'‘Nñ(ð$ ò 	#ˆCØ—‘˜cÑ"ð	#á™Ø�J‰J�Lð (øô ò 	Ü�N‰NÐ]Ð_bÔcÜûð	ús   “BC$ Â#C$ Ã$	DÃ-DÄDc                ó  — ddl m} d}|j                  �%	 | j                  ||j                   |«       d¬«      }|�K|j                  |j                  k7  r2t
        j                  d||j                  |d   |j                  «       |S |�&t
        j                  d	|j                  ||d   «       yt
        j                  d
||j                  |d   «       y# t        $ r,}t
        j                  d||j                  |«       Y d}~yd}~ww xY w)zrFor a routing entry whose row has ended: ``"prune"``, a replacement entry (repoint), or
        None (keep as-is).r   )Ú_nowNT)r>   ÚsourceÚnowÚraise_on_lookup_errorzRgateway.session: recovery lookup failed for stale sessions.json entry %r -> %s: %szfgateway.session: repointing stale sessions.json entry %r from ended %s (end_reason=%r) to recovered %sr§   zdgateway.session: reopened ended session %s for sessions.json entry %r (end_reason=%r); keeping routezfgateway.session: pruning stale sessions.json entry %r -> %s (end_reason=%r); left by a crashed gatewayr¨   )
Úgateway.session_lifecycler²   ÚoriginÚ_recover_session_from_dbr   rF   rG   rS   rK   r¡   )r$   rx   rT   r¯   r²   Úrecovered_entryr   s          r   rª   z,SessionPersistenceMixin._stale_entry_verdict]  s   € õ 	3ØˆØ�<‰<Ð#ðØ"&×"?Ñ"?Ø #¨E¯L©L¹d»fÐ\`ð #@ó #b�ð Ð&¨?×+EÑ+EÈ×IYÑIYÒ+YÜ�N‰Nð2Ø36¸×8HÑ8HÈ#ÈlÑJ[Ø×*Ñ*ô,ð #Ð"ð Ð&Ü�K‰Kð1Ø27×2BÑ2BÀCÈÈ\ÑIZô\ð Ü�‰ð Ø!$ e×&6Ñ&6¸¸LÑ8Iô	Kð øô7 ò ä—‘ðØ! 5×#3Ñ#3°Sô:ô ûðús   –$C Ã	D
Ã"DÄD
c                ó†   — | j                   j                  «       D ��ci c]  \  }}||j                  «       “Œ c}}S c c}}w )z1Serializable snapshot of ``_entries``. Lock held.)rQ   ru   Úto_dict)r$   rx   rT   s      r   rŽ   z)SessionPersistenceMixin._entries_as_dictsƒ  s2   € à7;·}±}×7JÑ7JÓ7L×M©¨¨e��U—]‘]“_Ñ$ÓMÐMùÓMs   ž=c                ó>   —  | j                   | j                  «       Ž  y)z;Persist the routing index while the caller holds ``_lock``.N)Ú_persist_routing_dataÚ_snapshot_routing_lockedr+   s    r   r¬   zSessionPersistenceMixin._save‡  s   € à"ˆ×"Ñ" D×$AÑ$AÓ$CÒDr   c                óD   — t        | dd«      dz   | _        | j                  S )zûBump and return the shared routing counter (lock held). Full snapshots AND single-entry
        fast saves MUST allocate from this one counter: the stale-write protection is a total order
        over serialization times and silently breaks otherwise.Ú_routing_generationr   r˜   )r)   rÀ   r+   s    r   Ú_next_routing_generation_lockedz7SessionPersistenceMixin._next_routing_generation_locked‹  s'   € ô $+¨4Ð1FÈÓ#JÈQÑ#NˆÔ Ø×'Ñ'Ð'r   c                ó  — t        | dd«      }t        | dd«      s|€y| j                  d«      }|€y	  || j                  «       ¬«      }| j                  «       }|j                  «       D ][  \  }}| j                  ||«      }|€Œ||vr| j                  j                  ||«       Œ<||vrŒA||   ||   k(  sŒM|| j                  |<   Œ] d| _        d| _        y# t        $ r }t        j                  d|«       Y d}~yd}~ww xY w)	z<Merge authoritative rows after a fallback-only startup load.r�   Nr�   Frr   rs   z;gateway.session: recovered state.db routing load failed: %sT)r)   rp   rk   r   rF   rK   rŽ   ru   rv   rQ   Ú
setdefaultr�   r�   )	r$   Úbaselinerw   Údurabler   Úcurrentrx   ry   Údurable_entrys	            r   r‰   z;SessionPersistenceMixin._reconcile_recovered_routing_locked’  s  € ä˜4Ð!=¸tÓDˆÜ�4Ð-¨uÔ5¸Ð9IØØ×(Ñ(Ð)GÓHˆØˆ>Øð	Ù 4×#6Ñ#6Ó#8Ô9ˆGð ×(Ñ(Ó*ˆØ&Ÿ}™}›ò 	3‰OˆC�Ø ×9Ñ9¸#¸zÓJˆMØÐ$ØØ˜(Ñ"ð —‘×(Ñ(¨¨mÕ<Ø˜GÑ#ØØ˜‘ ¨#¡Ó.Ø%2�—‘˜cÒ"ð	3ð #'ˆÔØ*.ˆÕ'øô# ò 	Ü�N‰NÐXÐZ]Ô^Üûð	ús   ³C Ã	D Ã C;Ã;D c                ób   — | j                  «        | j                  «       | j                  «       fS )z:Capture immutable routing data and a monotonic generation.)r‰   rŽ   rÁ   r+   s    r   r¾   z0SessionPersistenceMixin._snapshot_routing_locked¯  s+   € à×0Ñ0Ô2Ø×%Ñ%Ó'¨×)MÑ)MÓ)OÐOÐOr   c                óŒ  — | j                  dt        j                  «      5  |t        | dd«      k  r
	 ddd«       yt        | dd«      }|r9|j	                  «       D ]&  \  }\  }}||kD  sŒt        j                  |«      ||<   Œ( d}| j                  d«      }|�P	  ||j	                  «       D �	�
ci c]  \  }	}
|	t        j                  |
«      “Œ c}
}	| j                  «       ¬«       d	}t        | dd	«      s|s	 | j                  |«       || _        |r6|j	                  «       D �	��cg c]  \  }	\  }}||k  sŒ|	‘Œ c}}}	D ]  }||= Œ ddd«       yc c}
}	w # t        $ r }t        j                  d
|«       Y d}~Œ“d}~ww xY w# t        $ r#}|s‚ t        j                  d|«       Y d}~Œ¡d}~ww xY wc c}}}	w # 1 sw Y   yxY w)zASerialize all whole-index writers through one durable write lock.Ú
_save_lockÚ_persisted_routing_generationr   NÚ_fast_persisted_entriesFÚreplace_gateway_routing_entriesrs   Tz1gateway.session: state.db routing save failed: %sÚ_write_sessions_jsonzKgateway.session: sessions.json mirror save failed after state.db commit: %s)Ú_lazyÚ	threadingÚLockr)   ru   r}   r~   rp   Údumpsrk   r   rF   rK   Ú_save_sessions_jsonrË   )r$   r¤   Ú
generationÚfast_persistedrx   Úrevisionry   Údb_savedÚreplacerÚkÚvr   Úrevr—   s                 r   r½   z-SessionPersistenceMixin._persist_routing_data´  sÑ  € à�Z‰Z˜¤i§n¡nÓ5ñ "	,ØœW TÐ+JÈAÓNÒNØ÷"	,ð "	,ô
 % TÐ+DÀdÓKˆNÙØ3A×3GÑ3GÓ3Iò ;Ñ/�CÑ/˜( JØ *Ó,Ü$(§J¡J¨zÓ$:˜˜Sš	ð;ð ˆHØ×.Ñ.Ð/PÓQˆHØÐ#ð]Ù¸4¿:¹:»<×H±4°1°a˜a¤§¡¨A£Ñ.ÓHÐPT×PcÑPcÓPeÕfØ#�Hô �tÐ3°TÔ:Á(ð	#Ø×,Ñ,¨TÔ2ð 2<ˆDÔ.ñ Ø1?×1EÑ1EÓ1G×]Ð]¡+ !¡X c¨1È3ÐR\ÓK\šAÔ]ò ,�CØ& sÑ+ð,÷C"	,ð "	,ùó Iøä ò ]Ü—N‘NÐ#VÐX[×\Ñ\ûð]ûô
 !ò #Ù#Øô —N‘NðØ!÷#ñ #ûð#üô ^÷C"	,ð "	,úsŽ   ¡F:¼-F:Á*/F:ÂEÂ/ EÃEÃ)F:Ã9FÄ
F:Ä'F3
Ä8F3
Ä<F:ÅEÅ	FÅ!E<Å7F:Å<FÆF:Æ	F0ÆF+Æ&F:Æ+F0Æ0
F:Æ:Gc                óJ   — t        | j                  dz  dt        i|¥d¬«       y)zLWrite the legacy sessions.json mirror of the routing index (atomic + fsync).r”   Ú_READMEi€  )ÚmodeN)r	   ri   Ú_SESSIONS_JSON_README)r$   r¤   s     r   rÓ   z+SessionPersistenceMixin._save_sessions_jsonÚ  s&   € ä˜$×+Ñ+¨oÑ=À	ÔK`Ð?iÐdhÐ?iÐpuÖvr   c                ó�   — | j                   5  | j                  «       \  }}ddd«       | j                  «       y# 1 sw Y   ŒxY w)zESnapshot latest state under ``_lock`` and persist after releasing it.N)rc   r¾   r½   )r$   r¤   rÔ   s      r   Ú_save_entriesz%SessionPersistenceMixin._save_entriesÞ  sA   € à�Z‰Zñ 	?Ø#×<Ñ<Ó>ÑˆD�*÷	?à×"Ñ" 4¨Õ4÷	?ð 	?ús	   �<¼AF)r„   Ú	lock_heldc               óÖ  — |rt        j                  «       n| j                  }|5  | j                  j	                  |«      }|€
	 ddd«       y|�t        |«      n|j                  «       }t        j                  |«      | j                  «       }}ddd«       | j                  d«      }	|	� 	 | j                  dt        j                  «      5  t        | dd«      k\  r
	 ddd«       y| j                  dt
        «      }
|
j	                  |«      }|�|d   |k\  r
	 ddd«       y |	|| j                  «       ¬«       ||f|
|<   ddd«       y|�<|5  | j'                  «       }ddd«       t        |«      |<   | j)                  |«       y| j+                  «        y# 1 sw Y   �ŒxY w# 1 sw Y   yxY w# t         $ r!}t"        j%                  d||«       Y d}~Œ�d}~ww xY w# 1 sw Y   Œ�xY w)	uý  Persist ONE routing entry via UPSERT â€” the per-turn fast path (a full rewrite fsyncs a
        multi-MB sessions.json). The key -> session_id mapping never changes here: structural
        transitions use the full rewrite (which also refreshes the sessions.json mirror; it may lag
        in metadata only). The revision comes from the shared routing generation counter; under
        ``_save_lock`` the upsert is skipped if a full snapshot or a newer fast save of this key
        already persisted (the reverse case lives in ``_persist_routing_data``). No DB or a failed
        upsert falls back to the full rewrite. ``entry_data`` persists a candidate BEFORE it is
        published to the live entry (failure-atomic transitions); the fallback carries it too.NÚsave_gateway_routing_entryrÊ   rË   r   rÌ   rs   zagateway.session: single-entry routing save failed for %r (%s); falling back to full index rewrite)Ú
contextlibÚnullcontextrc   rQ   r#   r   r»   r}   rÒ   rÁ   rp   rÏ   rÐ   rÑ   r)   rk   r   rF   rK   rŽ   r½   rá   )r$   r>   r„   râ   ÚguardrT   Ú
serializedry   rÖ   ÚsaverrÕ   Ú	persistedr   Úfallback_datas                 r   Ú_save_entryz#SessionPersistenceMixin._save_entryä  sì  € ñ -6”
×&Ñ&Ô(¸4¿:¹:ˆØñ 	bØ—M‘M×%Ñ% kÓ2ˆEØˆ}Ø÷	bð 	bð .8Ð-Cœ˜jÔ)ÈÏÉËˆJä#'§:¡:¨jÓ#9¸4×;_Ñ;_Ó;a˜ˆJ÷	bð ×'Ñ'Ð(DÓEˆØÐð?Ø—Z‘Z ¬i¯n©nÓ=ñ IÜ˜tÐ%DÀaÓHÈHÒTØ÷Ið Ið &*§Z¡ZÐ0IÌ4Ó%P�NØ .× 2Ñ 2°;Ó ?�IØ Ð,°¸1±ÀÒ1IØ÷Ið Iñ ˜+ z¸×9LÑ9LÓ9NÕOØ3;¸ZÐ2H�N ;Ñ/÷Ið ð
 Ð!àñ 9Ø $× 6Ñ 6Ó 8�÷9ä)-¨jÓ)9ˆM˜+Ñ&Ø×&Ñ& }°hÕ?à×ÑÕ ÷?	bñ 	bú÷Ið ûÜò ?Ü—‘ð,Ø-8¸#÷?ñ ?ûð?ú÷9ð 9úsl   ¥FÁAFÂ+ F2 ÃF&ÃF2 Ã&2F&ÄF2 Ä! F&ÅF2 ÅGÆF#Æ&F/Æ+F2 Æ/F2 Æ2	GÆ;GÇGÇG(r1   )r%   úOptional[Path]r]   )r>   úOptional[str]r^   rî   )r>   rî   r^   rí   )r>   rî   )rS   rî   r^   rî   )rS   rî   )r>   r   r^   úOptional[SessionEntry])r^   r   )rn   r   )r^   r‹   )rx   r   ry   r   r^   rï   )r’   r‹   r^   r_   )rx   r   )r^   úDict[str, Any])r^   Úint)r^   ztuple[Dict[str, Any], int])r¤   rð   rÔ   rñ   r^   r_   )r¤   rð   r^   r_   )r>   r   r„   zOptional[Dict[str, Any]]râ   r‹   r^   r_   )$r    Ú
__module__Ú__qualname__Ú__doc__r&   r,   Úpropertyr/   Úsetterr8   r@   rI   rM   rU   rW   ra   re   rg   rk   rp   rz   Ústaticmethodrv   rd   rŒ   r�   rª   rŽ   r¬   rÁ   r‰   r¾   r½   rÓ   rá   rì   © r   r   r   r   (   s  „ Ùaô[ò09ð ñ^ó ð^ð 	‡Z�Zò ó ð ð ñó ðó.Hóó.&óPSóLó0ó)ó
.ó
*ó4ó
ð  ò	ó ð	ó,ó*Fó>ó@$óLNóEó(ó/ó:Pó
$,óLwó5ð KOØñ+!Øð+!Ø/Gð+!àð+!à$(ô+!r   r   )r   ÚBaseExceptionr^   r‹   )rô   Ú
__future__r   rå   Úloggingr}   rÐ   Úpathlibr   Útypingr   r   r   r   Úutilsr	   r|   r   Ú	getLoggerrF   Úobjectr*   rß   r   r   rø   r   r   ú<module>r     si   ðñDõ #ã Û Û Û Ý ß 5Ó 5Ý #áÝ,ð 
ˆ×	Ñ	Ð,Ó	-€ñ ‹x€ðEð óM÷
g!ò g!r   