+
    jHàj?7  ã                   óö   € R 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 ]P                  ! ]4      t]
tRtR tR t]! R	R
04      t ! R R4      tR tR tR tR tR tR tR tR tR tR t R# )aÞ  The last viewer closing a session ends it on the owner, lifecycle included (#134889 LC-1/LC-2/LC-6).

Main's classic CLI and TUI ran the end of a conversation in their own process: ``end_session``,
the memory providers' ``on_session_end`` extraction and plugin ``on_session_finalize`` (which also
closes the Relay conversation scope). Under the gateway those clients are viewers: the agent, its
memory manager and the Relay scope live in the owner, so ``session.detach {end_reason}`` from the
last viewer is where the owner runs them. A close that lands while a turn is still queued or running
is remembered and completed when the FIFO goes idle, so exiting mid-turn still ends the row.
The pending close is also kept in ``state_meta`` (``CLOSE_PREFIX + session_id``): a gateway stop or
restart before the FIFO goes idle would otherwise drop it, and nothing else ends a local row. Boot
recovery re-arms it (``rearm_persisted_closes``), so the restarted owner's first idle point ends it.
Each stored close carries its own operation id, and every delete is compare-and-delete on that exact
record under the owner-epoch fence: a cancelled older close, a reattach or a stale owner can only
remove the record it saw, never a newer close stored since.
N)Úpartial)Úlocal_receipt)ÚVIEWER_CLOSE_PREFIX)ÚRuntimeStoreErrorzäSELECT 1 FROM session_admissions WHERE target_session_id=? AND status IN ('queued','started','unknown') UNION ALL SELECT 1 FROM worker_executions WHERE session_id IN (?,?) AND status IN ('registered','running','unknown') LIMIT 1c                 óÎ   €  \        V P                  V4      R ,          R,          #   \        \        \        3 d)    T P                  P                  T4      ;'       g    Tu # i ; i)ÚentryÚ
session_id)r   Údbr   ÚKeyErrorÚ	TypeErrorÚget_compression_tip)Ú	authorityr   s   &&ÚA/home/ubuntu/.hermes/hermes-agent/gateway/session_viewer_close.pyÚ_physical_targetr   #   sX   € ðJÜ˜YŸ\™\¨:Ó6°wÕ?ÀÕMÐMøÜœx¬Ð3ô JØ�|‰|×/Ñ/°
Ó;×IÐI¸zÒIðJús   ‚#& ¦7A$ÁA$Á#A$c                 óÔ   € V P                   P                  4       ;_uu_ 4       pVP                  \        WV34      P	                  4       R JuuR R R 4       #   + '       g   i     R # ; i©N)r	   Ú	_read_ctxÚexecuteÚ	_BUSY_SQLÚfetchone)r   r   ÚtargetÚconns   &&& r   Ú_busyr   *   sH   € Ø	�‰×	Ñ	×	!Ô	! TØ�|‰|œI¨
ÀÐ'GÓH×QÑQÓSÐ[_Ð_÷ 
"×	!×	!Ó	!ús   £(AÁA'	Únew_sessionÚ	tui_closec                   óB   a € ] tR t^4t o RtRtRR lt]R 4       tRt	V t
R# )Ú_PendingClosez®One viewer close; identity, not the reason, tells a settler its close is still the pending one.
``record`` is its ``state_meta`` value: a unique operation id plus the reason.Nc                ó¢   € Wn         T;'       g:    \        P                  ! R \        P                  ! 4       P
                  RV/RR7      V n        R# )ÚopÚreasonT)Ú	sort_keysN)r   ÚjsonÚdumpsÚuuidÚuuid4ÚhexÚrecord)Úselfr   r&   s   &&&r   Ú__init__Ú_PendingClose.__init__9   s7   € ØŒØ×fÐf¤§
¢
¨D´$·*²*³,×2BÑ2BÀHÈfÐ+UÐaeÔ fˆŽó    c                óŠ   €  \         P                  ! V4      R ,          pV ! W!4      #   \        \        \        3 d    Tp L"i ; i)r   )r!   ÚloadsÚ
ValueErrorr   r
   )Úclsr&   r   s   && r   Úfrom_recordÚ_PendingClose.from_record=   sD   € ð	Ü—Z’Z Ó'¨Õ1ˆFñ �6Ó"Ð"øô œI¤xÐ0ô 	ØŠFð	ús   ‚' §AÁA)r   r&   r   )Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__Ú__doc__Ú	__slots__r(   Úclassmethodr/   Ú__static_attributes__Ú__classdictcell__)Ú__classdict__s   @r   r   r   4   s*   ø‡ € ñVà$€Iôgð ñ#ó ö#r*   r   c           	   ƒ  óœ  "  € V P                   V,          pVP                  '       d   R# VP                  '       d   R# ^ RIHpHp VP                  p\        \        P                  V\        W4      34      4      pV! V P                  V3WgR7       V F  pV! WhRV 2R7       K  	  \        V4      ;V P                  V&   p	\        P                  ! \         WV	P"                  4      G Rj  x€L
  \%        WW94      '       g1   \        P                  ! \&        WV	P"                  4      G Rj  x€L
  R# \)        W4      G Rj  x€L
  R#  L[ L L
5i)aq  ``session.detach {end_reason}`` left *session_id* without viewers: close it now, or once idle.
Its own async delegations end now, mid-turn too, as main's CLI exit and TUI close ended them
(#134889 LC-7); each returns as an interrupted completion, and ones its still-running turn starts
later are refused until a viewer attaches again. Other sessions' work is untouched.N)Úinterrupt_for_sessionÚrefuse_for_session)Úsession_keyÚparent_session_idszviewer_close:)r>   Úparent_session_idr   )ÚsessionsÚsubscribersÚ
supersededÚtools.async_delegationr<   r=   ÚrouteÚtupleÚdictÚfromkeysr   Ú
profile_idr   Úpending_closesÚasyncioÚ	to_threadÚ_persist_closer&   Ú_still_pendingÚforget_persisted_closeÚsettle_viewer_close)
r   r   r   Úliver<   r=   rE   ÚparentsÚparentÚcloses
   &&&       r   Úclose_viewer_sessionrU   F   s  é € ð
 ×Ñ˜jÕ)€DØ××ÐÙØ‡‡€ÙßPØ�J‰J€EÜ”D—M‘M :Ô/?À	Ó/VÐ"WÓXÓY€GÙ˜	×,Ñ,¨jÐ9ÀuÕiÛˆÙ¨%ÐTaÐbhÐaiÐRj×kñ ä3@ÀÓ3HÐH€I×Ñ˜ZÑ(¨5Ü
×
Ò
œN¨IÀ5Ç<Á<Ó
P×PÐPÜ˜)°×=Ò=ô ×ÒÔ 6¸	ÈuÏ|É|Ó\×\Ð\ÙÜ
˜iÓ
4×4Ò4ñ Qñ 	]á4ùs;   ‚9E¼B.EÃ*EÃ+?EÄ*EÄ+EÄ?E
Å EÅEÅ
Ec                 ób   a aaa€ ^ RI Ho VV VV3R lpS P                  P                  V4       R# )é    ©Ú_epochc                 ól   <€ S! V SP                   4       V P                  R \        S,           S34       R# )zaINSERT INTO state_meta(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.valueN)Úepochr   ÚCLOSE_PREFIX©r   rY   r   r&   r   s   &€€€€r   ÚwriteÚ_persist_close.<locals>.writec   s-   ø€ Ùˆt�Y—_‘_Ô%Ø�‰ÐxÜ" ZÕ/°Ð8ö	:r*   N)Úhermes_state_runtimerY   r	   Ú_execute_write)r   r   r&   r^   rY   s   fff @r   rM   rM   `   s$   û€ Ý+÷:ð :ð ‡L�L×Ñ Ö&r*   c                óÒ   a aaa€ ^ RI Ho VV VV3R lp \        S P                  P	                  V4      4      #   \
         d#   p\        P                  RST4        Rp?R# Rp?ii ; i)zÂBlocking: drop *session_id*'s stored close only if it is still exactly *record* (that close
completed or was cancelled), and only while this authority owns the store. Returns whether a
row went.rX   c                 ó|   <€ S! V SP                   4       V P                  R \        S,           S34      P                  # )z.DELETE FROM state_meta WHERE key=? AND value=?)r[   r   r\   Úrowcountr]   s   &€€€€r   r^   Ú%forget_persisted_close.<locals>.writep   s9   ø€ Ùˆt�Y—_‘_Ô%Ø�|‰|ÐLÜ)¨JÕ6¸Ð?óAßAIÁð	Jr*   z%Stored close for %s left in place: %sNF)r`   rY   Úboolr	   ra   r   ÚloggerÚinfo)r   r   r&   r^   ÚexcrY   s   fff  @r   rO   rO   j   sW   û€ õ ,÷Jð JðÜ�I—L‘L×/Ñ/°Ó6Ó7Ð7øÜô Ü�‰Ð;¸ZÈÔMÝûðús   •#9 ¹A&ÁA!Á!A&c                óÚ   € V P                   P                  \        V,           4      pVf   R# V P                  P	                  V4      pVe   VP
                  V8X  d   R# \        WV4       R# )z±Blocking, for a viewer attach: drop the stored close unless it is the one now pending in
memory (a detach that ran after the attach stored it; it is that close's own to settle).N)r	   Úget_metar\   rJ   Úgetr&   rO   )r   r   r&   Úpendings   &&  r   Úcancel_persisted_closern   {   sY   € ð �\‰\×"Ñ"¤<°*Õ#<Ó=€FØ‚~ÙØ×&Ñ&×*Ñ*¨:Ó6€GØÒ˜wŸ~™~°Ô7ÙÜ˜9°&Ö9r*   c                ó>  € V P                   P                  4       ;_uu_ 4       pVP                  R\        R,           34      P	                  4       pRRR4       . pX Fª  w  rEV\        \        4      R pV P                  P                  V4      pVe   VP                  '       d   KJ  VP                  '       d   \        WV4       Kj  V P                  P                  V\        P                  V4      4       VP                  V4       K¬  	  V#   + '       g   i     LÄ; i)a  Boot/serve recovery, after the local sessions are restored: every stored close whose session
is mounted with no viewer goes back to ``pending_closes``. The caller schedules the drain, whose
idle point (after any queued turn; never past an ``unknown`` one) completes it. Returns the ids.z2SELECT key, value FROM state_meta WHERE key LIKE ?Ú%N)r	   r   r   r\   ÚfetchallÚlenrA   rl   rC   rB   rO   rJ   Ú
setdefaultr   r/   Úappend)r   r   ÚstoredÚarmedÚkeyr&   r   rQ   s   &       r   Úrearm_persisted_closesrx   ‡   sâ   € ð 
�‰×	Ñ	×	!Ô	! TØ—‘ÐRÔUaÐdgÕUgÐTiÓj×sÑsÓuˆ÷ 
"à€EÛ‰ˆØœœ\Ó*Ð+Ð,ˆ
Ø×!Ñ!×%Ñ% jÓ1ˆØŠ<˜4Ÿ?Ÿ?˜?ÙØ××ÐÜ" 9¸&ÔAÙØ× Ñ ×+Ñ+¨J¼×8QÑ8QÐRXÓ8YÔZØ�‰�ZÖ ñ ð €L÷ 
"×	!ús   £-DÄD	c           	   ƒ  ó†  "  € V P                   P                  V4      pV P                  P                  V4      pVe   Ve   VP                  '       d   V P                   P	                  VR4       R# \        WW24      G Rj  x€L
 '       g   R# \        W4      p\        P                  ! \        WV4      G Rj  x€L
 '       d   R# \        P                  ! V P                  P                  V4      G Rj  x€L
 p\        WW24      '       g   R# T P                   V ^ RIHpHp ^ RIHp V! V \%        W`WP&                  4      \%        WpV4      R7      G Rj  x€L
 p	V	'       g   \)        WW4V4      G Rj  x€L
  R# \        P                  ! \*        WVP,                  4      G Rj  x€L
  VP                  '       d   R# T;'       g    / P                  R4      ;'       g5    VP.                  '       d!   VP.                  P0                  P2                  MRp
\5        WWJVP&                  4      G Rj  x€L
  R#  EL° EL| ELD Læ LË L� L5i)a  Complete a pending viewer close once *session_id* has no viewer and an idle FIFO: stamp the
row with the viewer's reason, commit the cached agent's memory and fire ``on_session_finalize``.
The drain calls this at every idle point; a viewer attaching meanwhile cancels the close.N)Úend_idle_viewer_sessionÚreopen_if_reattached)Útracked_write)ÚthenÚsourceÚ )rJ   rl   rA   rB   ÚpopÚ_resolve_abandoned_unknownr   rK   rL   r   r	   Úget_sessionrN   Úgateway.session_acp_lifecyclerz   r{   Úgateway.session_runtime_workersr|   r   r   Ú_requeue_if_busyrO   r&   r~   ÚplatformÚvalueÚ_run_close_lifecycle)r   r   rT   rQ   r   Úrowrz   r{   r|   Úendedr†   s   &&         r   rP   rP   ›   sÃ  é € ð ×$Ñ$×(Ñ(¨Ó4€EØ×Ñ×!Ñ! *Ó-€DØ‚}˜š¨×(8×(8Ð(8Ø× Ñ ×$Ñ$ Z°Ô6ÙÜ+¨IÀ4ÓO×OÖOÙÜ˜iÓ4€Fô ×Òœu i¸VÓD×DÖDÙÜ×!Ò! )§,¡,×":Ñ":¸FÓC×
C€CÜ˜)°×=Ò=Ùð 	× Ñ  Ð,ß[Ý=Ù 	¬7Ð3JÐWa×coÑcoÓ+pÜ%,Ð-AÈjÓ%Yô[÷ [€EçÜ˜y°dÀEÓJ×JÐJÙÜ
×
Ò
Ô2°IÈ5Ï<É<Ó
X×XÐXØ××ÐÙØ—	�	�r�‰˜xÓ(×_Ð_È4Ï;Ï;È;¨T¯[©[×-AÑ-A×-GÒ-GÐ\^€HÜ
˜y°À%Ç,Á,Ó
O×OÒOò1 Pò
 Eâ
Cñ[ñ 	KáXñ Pùs¯   ‚AIÁ-IÂ H0Â	IÂ,IÂ7H3Â8	IÃ0IÃ2H6Ã3AIÅH9ÅIÅIÅ/H;Å0/IÆH=Æ IÆ6
IÇIÇIÇ-<IÈ)H?È*IÈ3IÈ6IÈ9IÈ;IÈ=IÈ?Ic              ƒ  óh  "  € ^ RI Hp \        P                  ! W@P                  VR7      G Rj  x€L
 pV Uu. uF  qfR,          R8X  g   K  VNK  	  ppV'       g   R# ^ RIHp V! V P                  V4      p	V F_  p\        WW#4      '       g    R#  V P                  W–R	,          VR
,          4      G Rj  x€L
  \        P                  RTR	,          T4       Ka  	  \        WW#4      #  L¸u upi  L9  \         d5   p
\        P                  RTR	,          YP                  4        Rp
?
 R# Rp
?
ii ; i5i)a  A turn the previous owner lost (hard kill) recovers ``unknown`` and pauses the FIFO until
someone Discards it, but the last viewer closed the session: nobody is left to. The viewer's
close is that decision, so the owner resolves it as Discard does (interrupted, never replayed,
transcript boundary written) and the drain moves on; a queued successor still runs before the
close. A viewer back first cancels the close and keeps the decision. Returns whether the
close may go on to its idle check (no ``unknown`` turn is left).)Úlist_session_admissions)r   NÚstatusÚunknownT)Ú
SessionRefFÚadmission_idÚ
generationz4Unknown turn %s on closed session %s left paused: %szRUnknown turn %s resolved interrupted: its session %s was closed by its last viewer)r`   rŒ   rK   rL   r	   Úgateway.session_contractr�   rI   rN   Úresolve_unknown_as_ownerr   rg   Úwarningr   rh   )r   r   rQ   rT   rŒ   Úrowsr‰   rŽ   r�   Úrefri   s   &&&&       r   r�   r�   ¿   s  é € õ =Ü×"Ò"Ð#:¿L¹LÐU_Ô`×`€DÙ"ÓA™d�s¨(¥m°yÑ&@�sˆs™d€GÐAßÙÝ3Ù
�Y×)Ñ)¨:Ó
6€CÛˆÜ˜i°T×AÒAÚð	Ø×4Ñ4°S¸nÕ:MÈsÐS_ÕO`Óa×aÐaô
 	�‰ÐhØ˜Õ'¨ö	5ñ ô ˜)°Ó=Ð=ñ' aùÚAñ bøÜ ô 	Ü�N‰NÐQØ˜~Õ.°
¿J¹JôHæûð	üsb   ‚,D2®C'¯D2·C)ÁC)Á?D2Â#C0Â4C.Â5C0Â9/D2Ã)D2Ã.C0Ã0D/Ã;(D*Ä#D2Ä*D/Ä/D2c                 ón   € VP                   '       * ;'       d    V P                  P                  V4      VJ # r   )rB   rJ   rl   )r   r   rQ   rT   s   &&&&r   rN   rN   Ý   s0   € Ø×ÑÔ×UÐU I×$<Ñ$<×$@Ñ$@ÀÓ$LÐPUÐ$UÐUr*   c              ƒ  óx  "  € \         P                  ! \        WV4      G Rj  x€L
 '       gA   WP                  9  d/   \         P                  ! \        WVP
                  4      G Rj  x€L
  R# VP                  '       g   WP                  9   d   R# W@P                  V&   \        W4      G Rj  x€L
  R#  L‘ LO L
5i)z¶The stamp was refused. If input was admitted after the idle check, the close goes back to the
drain's next idle point; any other refusal (row already ended) leaves nothing to finish.N)rK   rL   r   rJ   rO   r&   rB   rP   )r   r   rQ   r   rT   s   &&&&&r   r…   r…   á   s’   é € ô ×"Ò"¤5¨)ÀÓH×HÖHØ×5Ñ5Ô5Ü×#Ò#Ô$:¸IÐSX×S_ÑS_Ó`×`Ð`ÙØ××Ð˜:×)AÑ)AÔAÙØ+0×Ñ˜ZÑ(ä
˜iÓ
4×4Ò4ñ Iá`ñ 5ùs>   ‚ B:¢B4£	B:­9B:Á&B6Á'B:Á?.B:Â-B8Â.B:Â6B:Â8B:c           	   ƒ  óŽ  "  € V P                   p\        VRR4      pVe   V! VP                  4       \        VRR4      pVf   R# V! W#WAP                  R7      G Rj  x€L
 pV'       d   \        P	                  RV4       \        VRR4      p	V\
        9   d)   V	e#   V	! VP                  RRVVRR7      G Rj  x€L
  R# R# R#  Ld L5i)	zÿWhat main's client ran at exit and ``/new``, in the process that owns the session: the
memory providers' end-of-session commit (the cached agent is evicted at a boundary, as a
gateway ``/new`` does) and the finalize hooks under the owning profile's scope.Ú_evict_cached_agent_at_boundaryNÚ_finalize_session_off_loop)r   r†   r   r>   z7Session-end plugin message(s) for %s dropped: no viewerÚ_reset_session_off_loopr   )r>   r   r   r†   Úold_session_idÚnew_session_id)ÚrunnerÚgetattrrE   rg   rh   Ú_RESET_REASONS)
r   rQ   r   r†   r   rŸ   ÚevictÚfinalizeÚmessagesÚresets
   &&&&&     r   rˆ   rˆ   ï   sË   é € ð ×Ñ€FÜ�FÐ=¸tÓD€EØÒÙˆd�j‰jÔÜ�vÐ;¸TÓB€HØÒÙÙ¨È6×_iÑ_iÔj×j€Hßä�‰ÐMÈvÔVÜ�FÐ5°tÓ<€EØ”Ô EÒ$5ñ  §
¡
°tÀMÐ\dØ#)¸$ô@÷ 	@ò 	@ñ %6Ññ kñ	@ùs+   ‚ACÁCÁCÁ)ACÂ6CÂ7CÃC)!r5   rK   r!   Úloggingr#   Ú	functoolsr   Úhermes_state_localr   Ú hermes_state_mutation_retirementr   r`   r   Ú	getLoggerr1   rg   r\   r   r   r   Ú	frozensetr¡   r   rU   rM   rO   rn   rx   rP   r�   rN   r…   rˆ   © r*   r   Ú<module>r­      sž   ðñó Û Û Û Ý å ,Ý @Ý 2à	×	Ò	˜8Ó	$€à"€ðH€	ò
Jò`ñ ˜M¨;Ð7Ó8€÷#ñ #ò$5ò4'òò"	:òò(!PòH>ò<Vò5ô@r*   