§
    øžyj[8  ã                   óÄ   — d Z ddlZddlZddlZddlmZmZ  ej        e¦  «        Z	dZ
dZ G d„ de¦  «        Z G d„ d	¦  «        Z G d
„ d¦  «        Z G d„ d¦  «        ZdS )ub  Per-session turn lease â€” serializes the [load history â†’ run â†’ flush] region.

Why this exists (#64934): the gateway's busy guards are keyed by ROUTING KEY
(``_active_sessions`` in the adapter, ``_running_agents`` in the runner), but
the durable transcript is owned by SESSION_ID â€” and ``switch_session()`` makes
the keyâ†’id mapping many-to-one (``/resume`` of a named session from a second
chat/topic, CLI-continuity rebinding, async-delegation completion pinning,
Telegram topic-binding tip-walks). Two routing keys mapped to one session_id
run concurrent turns on two different agent objects, so no per-key guard ever
sees the collision. The two turns then interleave their flushes on one
transcript: rows persist in completion order instead of arrival order, the
identity-marker dedup over shared history dicts can swallow a row outright,
and the second turn runs on a history base that never saw the first turn's
exchange â€” leaving a permanent ``user;user`` alternation wedge that
``repair_message_sequence`` re-repairs on every request forever.

The lease closes that route by serializing per RESOLVED session_id: it is
acquired after session resolution is final (post ``switch_session``/tip-walk),
immediately before the transcript load, and released in the dispatch layer's
``finally`` on every exit path. Same-key messages never reach the acquisition
point while a turn runs (both routing-key guards hold them), so the lock is
uncontended everywhere except the alias-key route â€” where the second turn now
waits for the first turn's flush and logs one WARNING naming the session and
both routing keys (pairing with the cross-agent tripwire in
``agent/agent_runtime_helpers.note_turn_start``).

Safety properties:

- **Generation-scoped, identity-checked release.** A token records its owner
  (routing key, run generation) and release only frees the lease when that
  exact token is the current holder â€” a stale unwind can never release a
  newer turn's lease (the #28686 ownership lesson applied). Release is
  idempotent.
- **Fail-closed on timeout.** A timed-out waiter raises
  :class:`TurnLeaseTimeoutError` and must be rejected by the dispatch layer
  with a visible resend notice. It never runs concurrently against the
  still-held lease and therefore cannot defeat the serialization invariant.
- **Bounded registry.** The per-session lease map is size-capped; eviction
  only ever removes idle (unheld, uncontended) entries, never a live lease.

Known limits (deliberate, flagged on #64934):

- A CLI process sharing the session via CLI-continuity is outside any
  in-process lock â€” that pair needs a DB-level lease (separate design).
- Mid-turn compression rotation leaves a small alias window: the tip-walk can
  resolve a fresh child id while the parent-holding turn is still in flight.
  The mid-turn binding-sync sites are the right place to alias the lease in a
  follow-up.
é    N)ÚDictÚOptionali   g      œ@c            
       ó8   ‡ — e Zd ZdZdededededdf
ˆ fd„Zˆ xZS )	ÚTurnLeaseTimeoutErrorzØThe session lease stayed held for the caller's full wait budget.

    This is a fail-closed signal: the caller did not acquire the lease and
    must not enter the transcript load/run/flush region for this turn.
    Ú
session_idÚ	owner_keyÚ
generationÚwait_secondsÚreturnNc                óž   •— || _         || _        || _        || _        t	          ¦   «                              d|d›d|› d|› d|› d�	¦  «         d S )Nz turn lease wait timed out after z.0fzs on session z for routing key z (gen ú))r   r   r	   r
   ÚsuperÚ__init__)Úselfr   r   r	   r
   Ú	__class__s        €ú8/home/ragecks/.hermes/hermes-agent/gateway/turn_lease.pyr   zTurnLeaseTimeoutError.__init__O   s’   ø€ ð %ˆŒØ"ˆŒØ$ˆŒØ(ˆÔÝ‰Œ×ÒðK¨|ÐNð Kð KØðKð KØ,5ðKð KØ=GðKð Kð Kñ	
ô 	
ð 	
ð 	
ð 	
ó    )	Ú__name__Ú
__module__Ú__qualname__Ú__doc__ÚstrÚintÚfloatr   Ú__classcell__)r   s   @r   r   r   H   sw   ø€ € € € € ðð ð
àð
ð ð	
ð
 ð
ð ð
ð 
ð
ð 
ð 
ð 
ð 
ð 
ð 
ð 
ð 
ð 
r   r   c                   ó:   — e Zd ZdZdZdedededdfd„Zdefd	„ZdS )
ÚTurnLeaseTokenzîHandle returned by :meth:`SessionTurnLeaseRegistry.acquire`.

    A timeout raises :class:`TurnLeaseTimeoutError` instead of returning a
    token, so every token handed out is a held lease. ``released`` makes
    release idempotent.
    ©r   r   r	   Úreleasedr   r   r	   r   Nc                 ó>   — || _         || _        || _        d| _        d S )NFr   )r   r   r   r	   s       r   r   zTurnLeaseToken.__init__k   s$   € ð %ˆŒØ"ˆŒØ$ˆŒØˆŒˆˆr   c           	      óH   — d| j         ›d| j        ›d| j        › d| j        › d�	S )NzTurnLeaseToken(session_id=z, owner_key=z, generation=z, released=r   r   ©r   s    r   Ú__repr__zTurnLeaseToken.__repr__v   sL   € ð)¨¬ð )ð )Øœð)ð )Ø8<¼ð)ð )àœð)ð )ð )ð	
r   )	r   r   r   r   Ú	__slots__r   r   r   r#   © r   r   r   r   a   s{   € € € € € ðð ð F€Ið	àð	ð ð	ð ð		ð
 
ð	ð 	ð 	ð 	ð
˜#ð 
ð 
ð 
ð 
ð 
ð 
r   r   c                   ó6   — e Zd ZdZdd„Zedefd„¦   «         ZdS )Ú_SessionLease)ÚlockÚholderÚacquired_atÚ	last_usedÚpending_acquiresr   Nc                 ó�   — t          j        ¦   «         | _        d | _        d| _        t          j        ¦   «         | _        d| _        d S )Nç        r   )ÚasyncioÚLockr(   r)   r*   Útimer+   r,   r"   s    r   r   z_SessionLease.__init__‡   s9   € Ý”L‘N”NˆŒ	Ø04ˆŒØˆÔÝœ™œˆŒØ !ˆÔÐÐr   c                 ó^   — | j         du o$| j                             ¦   «          o
| j        dk    S )z?True when this lease can be evicted: nobody holds or awaits it.Nr   )r)   r(   Úlockedr,   r"   s    r   Úidlez_SessionLease.idleŽ   s<   € ð ŒK˜4Ðð +Ø”I×$Ò$Ñ&Ô&Ð&ð+àÔ%¨Ò*ð	
r   ©r   N)r   r   r   r$   r   ÚpropertyÚboolr4   r%   r   r   r'   r'   ~   sV   € € € € € ð€Ið"ð "ð "ð "ð ð
�dð 
ð 
ð 
ñ „Xð
ð 
ð 
r   r'   c                   óÀ   — e Zd ZdZefdeddfd„Zdefd„Zdede	fd„Z
dd	„Zdd
œdedededee         dee         f
d„Zdee         dedefd„Zdee         defd„ZdS )ÚSessionTurnLeaseRegistryu  Asyncio lease per resolved session_id serializing transcript turns.

    Process-local and single-event-loop by design â€” the same visibility scope
    as the routing-key guards it extends. All methods must be called from the
    gateway's event loop.
    Úmax_entriesr   Nc                 óX   — i | _         t          dt          |¦  «        ¦  «        | _        d S )Né   )Ú_leasesÚmaxr   Ú_max_entries)r   r:   s     r   r   z!SessionTurnLeaseRegistry.__init__    s)   € Ø13ˆŒÝ ¥3 {Ñ#3Ô#3Ñ4Ô4ˆÔÐÐr   c                 ó*   — t          | j        ¦  «        S ©N)Úlenr=   r"   s    r   Ú__len__z SessionTurnLeaseRegistry.__len__¤   s   € Ý�4”<Ñ Ô Ð r   r   c                 óÆ   — | j                              |¦  «        }|€,|                      ¦   «          t          ¦   «         }|| j         |<   t	          j        ¦   «         |_        |S rA   )r=   ÚgetÚ_evict_idler'   r1   r+   )r   r   Úleases      r   Ú_get_or_createz'SessionTurnLeaseRegistry._get_or_create§   sU   € Ø”× Ò  Ñ,Ô,ˆØˆ=Ø×ÒÑÔÐÝ!‘O”OˆEØ',ˆDŒL˜Ñ$Ýœ)™+œ+ˆŒØˆr   c                 ó  ‡ — t          ‰ j        ¦  «        ‰ j        z
  dz   }|dk    rdS t          d„ ‰ j                             ¦   «         D ¦   «         ˆ fd„¬¦  «        }|d|…         D ]}‰ j                             |d¦  «         ŒdS )u“   Drop oldest idle entries so a new lease fits under the cap.

        Never evicts a held or contended lease â€” correctness beats the cap.
        r<   r   Nc              3   ó.   K  — | ]\  }}|j         ¯|V — Œd S rA   )r4   )Ú.0ÚsidrG   s      r   ú	<genexpr>z7SessionTurnLeaseRegistry._evict_idle.<locals>.<genexpr>¹   s-   è è € ÐFÐF‘Z�S˜%¸5¼:ÐFˆSÐFÐFÐFÐFÐFÐFr   c                 ó(   •— ‰j         |          j        S rA   )r=   r+   )rL   r   s    €r   ú<lambda>z6SessionTurnLeaseRegistry._evict_idle.<locals>.<lambda>º   s   ø€ ˜DœL¨Ô-Ô7€ r   )Úkey)rB   r=   r?   ÚsortedÚitemsÚpop)r   ÚoverflowÚidle_idsrL   s   `   r   rF   z$SessionTurnLeaseRegistry._evict_idle°   s­   ø€ õ
 �t”|Ñ$Ô$ tÔ'8Ñ8¸1Ñ<ˆØ�qŠ=ˆ=ØˆFÝØFÐF 4¤<×#5Ò#5Ñ#7Ô#7ÐFÑFÔFØ7Ð7Ð7Ð7ð
ñ 
ô 
ˆð ˜I˜X˜IÔ&ð 	(ð 	(ˆCØŒL×Ò˜S $Ñ'Ô'Ð'Ð'ð	(ð 	(r   ©Útimeoutr   r	   rW   c          
   ƒ   óŠ  K  — |sdS |r|dk    rt          |¦  «        nt          }t          ||t          |¦  «        ¦  «        }|                      |¦  «        }|j                             ¦   «         r[|j        }t           	                    d||||r|j
        nd|r|j        nd|j        rt          j        ¦   «         |j        z
  nd¦  «         |xj        dz  c_        	 t          j        |j                             ¦   «         |¬¦  «        ƒ d{V —† n`# t          j        $ rN |j        }t                               d|||||r|j
        nd|r|j        nd¦  «         t)          ||||¬	¦  «        d‚w xY w	 |xj        dz  c_        n# |xj        dz  c_        w xY w||_        t          j        ¦   «         |_        |j        |_        |S )
a>  Acquire the turn lease for ``session_id``, waiting if held.

        Returns a held :class:`TurnLeaseToken`. Raises
        :class:`TurnLeaseTimeoutError` when the wait budget expires; the caller
        must reject rather than enter the serialized region. Returns ``None``
        for a falsy ``session_id``.
        Nr   uù   turn lease contention on session %s: routing key %s (gen %s) waiting behind in-flight turn held by routing key %s (gen %s, held %.0fs) â€” two routing keys are mapped to one session_id (#64934); serializing this turn behind the previous turn's flushú?g      ð¿r<   rV   uÊ   turn lease wait timed out after %.0fs on session %s (waiter: routing key %s gen %s; holder: routing key %s gen %s) â€” failing closed: refusing to run this turn UNSERIALIZED against the still-held lease)r   r	   r
   )r   ÚDEFAULT_LEASE_WAITr   r   rH   r(   r3   r)   ÚloggerÚwarningr   r	   r*   r1   r,   r/   Úwait_forÚacquireÚTimeoutErrorÚerrorr   r+   )	r   r   r   r	   rW   ÚwaitÚtokenrG   r)   s	            r   r^   z SessionTurnLeaseRegistry.acquire¿   s.  è è € ð ð 	Ø�4Ø!(ÐP¨W°qª[¨[�u�W‰~Œ~ˆ~Õ>PˆÝ˜z¨9µc¸*±o´oÑFÔFˆØ×#Ò# JÑ/Ô/ˆàŒ:×ÒÑÔð 	Ø”\ˆFÝ�NŠNðð
 ØØØ$*Ð3�Ô Ð °Ø%+Ð4�Ô!Ð!°Ø38Ô3DÐN•”	‘”˜eÔ/Ñ/Ð/È$ñô ð ð& 	ÐÔ !Ñ#ÐÔð	(ÝÔ" 5¤:×#5Ò#5Ñ#7Ô#7ÀÐFÑFÔFÐFÐFÐFÐFÐFÐFÐFÐFøÝÔ#ð 	ð 	ð 	Ø”\ˆFÝ�LŠLð<ð ØØØØ$*Ð3�Ô Ð °Ø%+Ð4�Ô!Ð!°ñô ð õ (ØØ#Ø%Ø!ð	ñ ô ð
 ðð	øøøð Gð, Ð"Ô" aÑ'Ð"Ô"Ð"øˆEÐ"Ô" aÑ'Ð"Ô"Ð"Ð"Ð"Ð"ð ˆŒÝ œI™KœKˆÔØÔ+ˆŒØˆs   Ã3D ÄF ÄAE.Å.F ÆFrb   Únew_session_idc           
      óÄ  — |�|j         s|r||j        k    rdS | j                             |j        ¦  «        }|�	|j        |urdS | j                             |¦  «        }|�V||urR|j        sK|j        }t                               d|j        ||j        |j	        |r|j        nd|r|j	        nd|¦  «         dS || j        |<   t          j
        ¦   «         |_        ||_        dS )u  Alias a HELD lease onto ``new_session_id`` after mid-turn rotation.

        Compression can rotate the durable session_id while a turn is in
        flight (session-hygiene pre-compression, in-agent compression). The
        turn's flush then targets the NEW id â€” so the serialization boundary
        must follow it, or an alias routing key resolving the new id (e.g. a
        topic tip-walk landing on the fresh child) could start a concurrent
        turn the lease never sees. This closes the rotation-alias window
        flagged on #64934.

        Mechanism: the SAME ``_SessionLease`` object is registered under the
        new id (the old mapping stays until it goes idle and is evicted), so
        acquirers on either id serialize against one lock â€” no lock state is
        moved, no asyncio internals are touched. Only the current holder can
        rebind (identity-checked like release), and the token follows to the
        new id so release frees the shared object.

        Edge: if the new id already has a live lease of its own (another
        turn is running on the target session), the two serialization
        domains cannot be merged mid-wait â€” log loudly and keep the token on
        the old id. Fail-open, never deadlock: a holder cannot wait mid-turn.
        NFu  turn lease rebind blocked: session %s rotated to %s mid-turn (holder: routing key %s gen %s) but the target session's lease is already live (holder: routing key %s gen %s) â€” keeping the lease on the old id; transcript writes on %s may interleave (#64934 rotation-alias edge)rY   T)r   r   r=   rE   r)   r4   r[   r\   r   r	   r1   r+   )r   rb   rc   rG   Úexistingr)   s         r   ÚrebindzSessionTurnLeaseRegistry.rebind
  s  € ð0 ˆMØŒ~ð à!ð ð  Ô!1Ò1Ð1à�5Ø”× Ò  Ô!1Ñ2Ô2ˆØˆ=˜EœL°Ð5Ð5Ø�5à”<×#Ò# NÑ3Ô3ˆØÐ H°EÐ$9Ð$9À(Ä-Ð$9Ø”_ˆFÝ�NŠNð>ð
 Ô ØØ”ØÔ Ø$*Ð3�Ô Ð °Ø%+Ð4�Ô!Ð!°Øñô ð ð �5à',ˆŒ�^Ñ$Ýœ)™+œ+ˆŒØ)ˆÔØˆtr   c                 óŽ  — |�|j         rdS d|_         | j                             |j        ¦  «        }|€dS |j        |ur.t
                               d|j        |j        |j        ¦  «         dS d|_        d|_	        t          j
        ¦   «         |_        |j                             ¦   «         r|j                             ¦   «          dS )uZ  Release ``token``'s lease. Idempotent; ownership-checked.

        Returns True only when this exact token was the current holder and
        the lock was freed. A re-release or a stale token whose slot has
        since been granted to a newer turn are both safe no-ops â€” a stale
        unwind can never release a newer turn's lease.
        NFTzYturn lease release skipped on session %s: token (key %s gen %s) is not the current holderr.   )r   r=   rE   r   r)   r[   Údebugr   r	   r*   r1   r+   r(   r3   Úrelease)r   rb   rG   s      r   ri   z SessionTurnLeaseRegistry.releaseD  sÈ   € ð ˆ=˜EœNˆ=Ø�5ØˆŒØ”× Ò  Ô!1Ñ2Ô2ˆØˆ=Ø�5ØŒ<˜uÐ$Ð$Ý�LŠLð4àÔ Ø”ØÔ ñô ð ð �5ØˆŒØˆÔÝœ)™+œ+ˆŒØŒ:×ÒÑÔð 	!ØŒJ×ÒÑ Ô Ð Øˆtr   r5   )r   r   r   r   ÚDEFAULT_MAX_LEASESr   r   rC   r   r'   rH   rF   r   r   r   r^   r7   rf   ri   r%   r   r   r9   r9   ˜   sY  € € € € € ðð ð +=ð 5ð 5 Cð 5Àð 5ð 5ð 5ð 5ð!˜ð !ð !ð !ð !ð¨ð °ð ð ð ð ð(ð (ð (ð (ð* $(ðIð Ið IàðIð ð	Ið
 ðIð ˜%”ðIð 
�.Ô	!ðIð Ið Ið IðV8˜H ^Ô4ð 8Àcð 8Èdð 8ð 8ð 8ð 8ðt˜X nÔ5ð ¸$ð ð ð ð ð ð r   r9   )r   r/   Úloggingr1   Útypingr   r   Ú	getLoggerr   r[   rj   rZ   r_   r   r   r'   r9   r%   r   r   ú<module>rn      s  ðð0ð 0ðd €€€Ø €€€Ø €€€Ø !Ð !Ð !Ð !Ð !Ð !Ð !Ð !à	ˆÔ	˜8Ñ	$Ô	$€ð Ð ð Ð ð
ð 
ð 
ð 
ð 
˜Lñ 
ô 
ð 
ð2
ð 
ð 
ð 
ð 
ñ 
ô 
ð 
ð:
ð 
ð 
ð 
ð 
ñ 
ô 
ð 
ð4Hð Hð Hð Hð Hñ Hô Hð Hð Hð Hr   