Ë
    âß²jA  ã                   óp  — d Z ddlZddlZddlZddlZddlZddlmZmZ ddl	m
Z
 dZdZdedefd	„Zd
Z ee
d   d   «      Z ee
d   d   «      ZdZ ee
d   d   «      Z ee
d   d   «      ZdZdZdZdZdZdZ ej8                  dej:                  «      Zdededz  fd„Z dKdee!e!f   dz  de!dz  fd„Z"	 dKddejF                  dœde!dz  dee!e!f   dz  dedz  ded    dedz  f
d!„Z$ed"œd#ed$edz  d%edefd&„Z%d'edefd(„Z&d'ed)edefd*„Z'h d+£Z(d,e!dz  de)fd-„Z*dKdee!e!f   dz  de)fd.„Z+dKdee!e!f   dz  de)fd/„Z,de)fd0„Z-dLd2ed3edefd4„Z.d5d6œd7ed8ed9e)defd:„Z/d7edefd;„Z0d7edefd<„Z1d7edefd=„Z2d7edefd>„Z3d1ed?œd#eded)ed@ed%edefdA„Z4efeeedBœd#eded%edCedDedefdE„Z5dFdGœd#edHedIedefdJ„Z6y)MzBShared gateway restart constants and supervisor detection helpers.é    N)ÚCallableÚMapping)ÚDEFAULT_CONFIGéK   éN   Ú
returncodeÚreturnc                 ó   — | t         k(  ry| S )uþ  Translate gateway EX_CONFIG so launchd can park the job.

    systemd uses ``RestartPreventExitStatus=78``; s6 maps 78â†’125. launchd cannot
    gate on a specific status, and unconditional ``KeepAlive=true`` respawns 78
    forever (#89477). The generated plist uses ``KeepAlive.SuccessfulExit=false``;
    mapping 78â†’0 is a deliberate stop. Exit 75 (please restart) and other
    failures pass through so KeepAlive still relaunches them. Negative ``wait()``
    codes (signals) are left to the caller.
    r   )ÚGATEWAY_FATAL_CONFIG_EXIT_CODE)r   s    ú4/home/ubuntu/.hermes/hermes-agent/gateway/restart.pyÚ!map_fatal_config_exit_for_launchdr      s   € ð Ô3Ò3ØØÐó    Ú"HERMES_GATEWAY_EXTERNAL_SUPERVISORÚagentÚrestart_drain_timeoutÚgatewayÚsignal_interrupt_grace_timeoutg      @Úrestart_after_turn_timeoutÚcron_drain_timeoutg      $@g      >@g      N@é<   g       @z ^\s*exit timeout\s*=\s*(\d+)\s*$Úprint_outputc                 ó€   — t         j                  t        | xs d«      «      }|€yt        |j	                  d«      «      S )z=Extract ``exit timeout = N`` from ``launchctl print`` output.Ú Né   )Ú_LAUNCHD_EXIT_TIMEOUT_REÚsearchÚstrÚfloatÚgroup)r   Úmatchs     r   Úparse_launchd_exit_timeoutr!   O   s7   € ä$×+Ñ+¬C°Ò0BÀÓ,CÓD€EØ€}ØÜ�—‘˜Q“Ó Ð r   Úenvironc                 óÔ   — t         j                  dk7  ry| €t        j                  n| }t	        |j                  dd«      xs d«      j                  «       }|j                  d«      sy|S )uâ  Return this process's ``ai.hermes.*`` launchd job label, or ``None``.

    Only labels of the gateway's own jobs count (same predicate as
    :mod:`gateway.control_socket`). App-coalition labels
    (``application.<bundle>â€¦``, exported into IDE integrated terminals) also
    populate ``XPC_SERVICE_NAME``, and ``launchctl print`` reports
    ``exit timeout = 1`` for them â€” treating those as a budget would cap the
    drain to 0 for a gateway Ctrl+C'd in such a terminal.
    ÚdarwinNÚXPC_SERVICE_NAMEr   z	ai.hermes)ÚsysÚplatformÚosr"   r   ÚgetÚstripÚ
startswith)r"   ÚenvÚlabels      r   Úlaunchd_service_labelr.   W   s\   € ô ‡|�|�xÒØØ˜Œ"�*Š*¨W€CÜ�—‘Ð*¨BÓ/Ò5°2Ó6×<Ñ<Ó>€EØ×Ñ˜KÔ(ØØ€Lr   )r"   ÚuidÚrunr-   r/   r0   ).z subprocess.CompletedProcess[str]c                ó8  — | xs t        |«      } | sy|€t        t        dd«      }|€y |«       }|dk(  rdnd|› �}	  |dd|› d| › �gd	d	d
dd¬«      }|j                  dk7  ryt        |j                  «      S # t        t        j
                  t        f$ r Y yw xY w)u¢  Live ``ExitTimeOut`` (seconds) launchd enforces for this gateway's job.

    Returns ``None`` â€” meaning "no launchd budget applies" â€” when the process
    is not launchd-owned (non-darwin, or no ``ai.hermes`` ``XPC_SERVICE_NAME``), ``launchctl`` is missing
    or fails, or the print output carries no ``exit timeout`` line. Callers
    must treat ``None`` as fail-open: the configured drain stands unchanged.
    NÚgetuidr   Úsystemzgui/Ú	launchctlÚprintú/Tzutf-8Úreplaceé   )Úcapture_outputÚtextÚencodingÚerrorsÚtimeout)
r.   Úgetattrr(   ÚOSErrorÚ
subprocessÚSubprocessErrorÚ
ValueErrorr   r!   Ústdout)r-   r"   r/   r0   r2   ÚdomainÚprocs          r   Úread_launchd_exit_timeout_srF   j   sÃ   € ð Ò3Ô*¨7Ó3€EÙØØ
€{Üœ˜X tÓ,ˆØˆ>ØÙ‹hˆØ š‰X¨¨c¨U |€Fð
ÙØ˜' f X¨Q¨u¨gÐ#6Ð7ØØØØØô
ˆð ‡�˜!ÒØÜ% d§k¡kÓ2Ð2øô	 ”Z×/Ñ/´Ð<ò Ùðús   ½A8 Á8BÂB©Úcleanup_reserve_sÚdrain_timeoutÚlaunchd_exit_timeout_srH   c          	      ó€   — t        | «      }t        |«      }|dk  r|S t        |t        |t        |«      z
  d«      «      S )ub  Clamp a SIGTERM-driven stop drain to what launchd will actually allow.

    ``launchd_exit_timeout_s`` is the live ``exit timeout`` for this job (see
    :func:`read_launchd_exit_timeout_s`); ``None`` means no launchd budget
    applies and the configured drain is returned untouched. Otherwise the
    drain may use at most ``exit_timeout - cleanup_reserve_s`` so the
    post-drain teardown (interrupt agents, disconnect adapters, checkpoint
    and close SQLite) still completes before launchd escalates to SIGKILL.
    Never *extends* the drain â€” an operator who configured a short one
    keeps it.
    ç        )Ú_secondsÚminÚmax)rI   rJ   rH   ÚdrainÚbudgets        r   Úresolve_launchd_capped_drainrR   ‘   sD   € ô$ �]Ó#€EÜÐ,Ó-€FØ�‚}ØˆÜˆu”c˜&¤8Ð,=Ó#>Ñ>ÀÓDÓEÐEr   Úrunnerc                 óp   — t        | dt        «      }t        | dd«      s|S t        |t        | dd«      «      S )uÝ  Drain budget for the stop in progress on ``runner``.

    Signal-driven stops under launchd are timed by launchd's live
    ``ExitTimeOut`` (``runner._launchd_exit_timeout_s``, set at boot);
    everything else â€” in-band SIGUSR1 restart after the turn, ``--replace``
    takeover, tests â€” keeps the configured drain. Duck-typed and
    getattr-guarded on purpose: shutdown-path tests drive ``_stop_impl``
    from bare doubles that are not ``GatewayRunner`` instances.
    Ú_restart_drain_timeoutÚ_stop_requested_by_signalFÚ_launchd_exit_timeout_sN)r>   Ú%DEFAULT_GATEWAY_RESTART_DRAIN_TIMEOUTrR   )rS   rP   s     r   Úeffective_stop_drain_timeoutrY   ª   s>   € ô �FÐ4Ô6[Ó\€EÜ�6Ð6¸Ô>ØˆÜ'¨¬w°vÐ?XÐZ^Ó/_Ó`Ð`r   Úwatchdog_delayc                 óp   — t        |«      }t        | dd«      s|S t        |t        | dd«      t        ¬«      S )uC  Thread-watchdog leash for the stop in progress on ``runner``.

    ``watchdog_delay`` is the supervisor-agnostic leash (effective drain +
    grace). Under launchd a signal-driven stop is SIGKILLed at the live
    ``ExitTimeOut`` â€” with a 60s budget the default leash (50 + 60 = 110s)
    can never fire, so the forensic stack dump and ``os._exit`` the watchdog
    exists for are lost. Clamp the leash to ``ExitTimeOut -
    LAUNCHD_WATCHDOG_DUMP_MARGIN_S`` so the dump lands before SIGKILL. Same
    duck-typing / fail-open rules as :func:`effective_stop_drain_timeout`.
    rV   FrW   NrG   )rM   r>   rR   ÚLAUNCHD_WATCHDOG_DUMP_MARGIN_S)rS   rZ   Úleashs      r   Úeffective_stop_watchdog_delayr^   º   s?   € ô �^Ó$€EÜ�6Ð6¸Ô>ØˆÜ'ØŒw�vÐ8¸$Ó?ÔSqôð r   >   Ú1ÚonÚyesÚtrueÚ
error_codec                 ó�   — | xs dj                  «       j                  «       }t        |«      xr |dk(  xs |j                  d«      S )aì  True when an adapter's fatal error is a single-writer ownership conflict.

    Adapters emit ``{scope}_lock`` with ``retryable=True`` so a *mid-run* reconnect can
    recover; at startup a live foreign holder is a configuration conflict (two gateways
    cannot poll one token), not a transient blip.  Matches by error CODE only, never text.

    ``BasePlatformAdapter._acquire_platform_lock`` emits ``{scope}_lock`` with ``retryable=True`` on
    purpose: a *mid-run* reconnect must be able to recover once the live holder exits or a stale record is
    cleared (#54167). This matches by error CODE only (the ``{scope}_lock`` / ``lock_conflict`` families
    every adapter emits for scoped-lock and identity conflicts), never by message text.
    r   Úlock_conflictÚ_lock)r*   ÚlowerÚboolÚendswith)rc   Úcodes     r   Úis_global_startup_conflictrk   Ð   sB   € ð Ò˜"×#Ñ#Ó%×+Ñ+Ó-€DÜ�‹:ÒM˜4 ?Ñ2ÒL°d·m±mÀGÓ6LÐMr   c                 óD  — | €t         j                  n| }|j                  dd«      }t        |j                  d«      xs_ |j                  d«      xsL |xr |dk7  xsA t	        |j                  t
        d«      «      j                  «       j                  «       t        v «      S )a7  Return whether this gateway process is owned by a supervisor that RESTARTS it.

    Selects the exit-75 restart route, so only markers of a manager with a restart policy count:
    systemd ``INVOCATION_ID``, launchd ``XPC_SERVICE_NAME``, the s6 sentinel, or the explicit
    ``--external-supervisor`` opt-in. The generalized ``HERMES_SUPERVISED_CHILD`` launcher marker is
    deliberately NOT read here: the Windows Scheduled-Task launcher sets it without a restart policy
    (#113670), and routing its ``/restart`` through exit 75 would leave the gateway dead.
    r%   r   ÚINVOCATION_IDÚHERMES_S6_SUPERVISED_CHILDÚ0)	r(   r"   r)   rh   r   ÚEXTERNAL_GATEWAY_SUPERVISOR_ENVr*   rg   Ú_TRUTHY)r"   r,   Úxpc_services      r   Úis_gateway_supervisor_processrs   à   s—   € ð  ˜Œ"�*Š*¨W€CØ—'‘'Ð,¨bÓ1€KÜ�—‘˜Ó(ò `¨C¯G©GÐ4PÓ,Qò `ÐVaÒVxÐfqÐuxÑfxò `Ü�s—w‘wÔ>ÀÓCÓD×JÑJÓL×RÑRÓTÔX_Ð_óað ar   c                 ód   — | €t         j                  n| }|j                  d«      ryt        | «      S )uS  Return whether this gateway was launched by a generated service/launcher rather than a shell.

    Superset of :func:`is_gateway_supervisor_process` that also honours ``HERMES_SUPERVISED_CHILD``,
    the marker every generated launcher exports (systemd unit, launchd plist, s6 run script, Windows
    Scheduled Task â€” see ``hermes_cli.main._apply_profile_override``). This is the identity the
    self-targeting guards key on: a kill or lifecycle command issued from inside such a gateway takes
    down the process hosting the caller with nobody at a terminal to bring it back (#113667).
    ÚHERMES_SUPERVISED_CHILDT)r(   r"   r)   rs   )r"   r,   s     r   Úis_supervised_gateway_launchrv   ï   s.   € ð  ˜Œ"�*Š*¨W€CØ
‡w�wÐ(Ô)ØÜ(¨Ó1Ð1r   c                  ó‚   — t         j                  j                  d«      xs t         j                  j                  d«      S )z±In a container (Docker/Podman) the detached setsid restart path dies with the cgroup,
    so exit-75 service restart is the only viable path.  Own function so tests can mock it.z/.dockerenvz/run/.containerenv)r(   ÚpathÚexists© r   r   Úis_container_restart_contextr{   þ   s*   € ô �7‰7�>‰>˜-Ó(ÒP¬B¯G©G¯N©NÐ;OÓ,PÐPr   rL   ÚvalueÚfallbackc                 ó\   — 	 t        t        | «      d«      S # t        t        f$ r |cY S w xY w)z9Non-negative float, or ``fallback`` on non-numeric input.rL   )rO   r   Ú	TypeErrorrB   )r|   r}   s     r   rM   rM     s1   € ðÜ”5˜“< Ó%Ð%øÜ”zÐ"ò ØŠðús   ‚ —+ª+F©ÚfiniteÚrawÚdefaultr�   c                óÚ   — | � t        | t        «      r| j                  «       s|S 	 t        | «      }|rt        j                  |«      s|S t        d|«      S # t        t
        f$ r |cY S w xY w)z—Parse a timeout where ``0`` is a deliberate disable (must NOT fall through
    to ``default``), unlike None / blank / non-numeric (/ non-finite) input.rL   )	Ú
isinstancer   r*   r   r   rB   ÚmathÚisfiniterO   )r‚   rƒ   r�   r|   s       r   Ú_parse_timeout_keeping_zerorˆ     sh   € ð €{”z #¤sÔ+°C·I±I´KØˆðÜ�c“
ˆñ ¤T§]¡]°5Ô%9ˆ7ÐN¼sÀ3È»ÐNøô ”zÐ"ò ØŠðús   ¦A ÁA*Á)A*c                 ó*   — t        | xs dt        «      S )zWParse a configured drain timeout; falsy (incl. ``0``) falls back to the shared default.N)rˆ   rX   ©r‚   s    r   Úparse_restart_drain_timeoutr‹     s   € ä& s¢{¨dÔ4YÓZÐZr   c                 ó"   — t        | t        «      S )zSParse the after-turn wait cap for in-band restart (``0`` = legacy immediate drain).)rˆ   Ú*DEFAULT_GATEWAY_RESTART_AFTER_TURN_TIMEOUTrŠ   s    r   Ú parse_restart_after_turn_timeoutrŽ     s   € ä& sÔ,VÓWÐWr   c                 ó"   — t        | t        «      S )u)  Parse the cron-only drain floor (``0`` = opt out; cron interrupted on the chat budget).

    ``0`` is a deliberate opt-out â€” cron work is then interrupted on the same budget as chat work, the
    pre-#82161 behaviour â€” and must not fall through to the default, unlike empty/missing input.
    )rˆ   Ú"DEFAULT_GATEWAY_CRON_DRAIN_TIMEOUTrŠ   s    r   Úparse_cron_drain_timeoutr‘   "  s   € ô ' sÔ,NÓOÐOr   c                 ó&   — t        | t        d¬«      S )z9Parse the unexpected-signal post-interrupt grace timeout.Tr€   )rˆ   Ú.DEFAULT_GATEWAY_SIGNAL_INTERRUPT_GRACE_TIMEOUTrŠ   s    r   Ú$parse_signal_interrupt_grace_timeoutr”   +  s   € ä& sÔ,ZÐcgÔhÐhr   )ÚelapsedrH   r•   c                ó¸   — t        | «      }t        |«      }|dk  r|S t        |«      t        |«      z
  t        |t        «      z
  }t        |t        ||«      «      S )aP  Seconds the stop drain may wait on in-flight cron work.

    Clamped to what this process can honour: the watchdog hard-exits at ``watchdog_delay``,
    so waiting past that leash minus ``cleanup_reserve_s`` swaps a cleanly-interrupted job
    for a SIGKILL that leaves it wedged.  Never less than ``drain_timeout`` (only extends).
    rL   )rM   ÚCRON_DRAIN_CLEANUP_RESERVE_SrO   rN   )rI   r   rZ   r•   rH   rP   ÚfloorÚceilings           r   Úresolve_cron_drain_budgetrš   0  s[   € ô �]Ó#€EÜÐ'Ó(€EØ�‚|ØˆÜ�~Ó&¬°'Ó):Ñ:¼XÐFWÔYuÓ=vÑv€GÜˆu”c˜% Ó)Ó*Ð*r   )rH   Ú
headroom_sÚfloor_sr›   rœ   c          	      óÀ   — t        | «      }t        |«      }|dkD  r|t        |«      z   nd}t        t        t        |«      t        ||«      t        |«      z   «      «      S )af  Seconds systemd ``TimeoutStopSec`` must cover: the stop path may first wait
    ``cron_drain_timeout`` + ``cleanup_reserve_s`` for cron work, so sizing from the chat drain
    alone lets systemd SIGKILL an in-budget drain.  A zero cron timeout is an opt-out.

    ``restart_drain_timeout`` is only the chat-turn interrupt budget (default 0). See #94759.
    rL   )rM   ÚintrO   )rI   r   rH   r›   rœ   rP   ÚcronÚcron_budgets           r   Ú resolve_systemd_timeout_stop_secr¡   B  sY   € ô �]Ó#€EÜÐ&Ó'€DØ:>Àº*�4œ(Ð#4Ó5Ò5È#€KÜŒs”8˜GÓ$¤c¨%°Ó&=ÄÈÓ@TÑ&TÓUÓVÐVr   g      .@)ÚheadroomÚafter_turn_timeoutr¢   c                óH   — t        | «      t        |«      z   t        |«      z   S )u¸   Seconds a CLI should wait for the gateway PID to exit after SIGUSR1: in-band restart may
    defer ``stop()`` until turns finish, then spend ``drain_timeout`` inside it â€” cover both.)rM   )rI   r£   r¢   s      r   Ú resolve_restart_exit_wait_budgetr¥   S  s$   € ô �MÓ"¤XÐ.@Ó%AÑAÄHÈXÓDVÑVÐVr   )N)rL   )7Ú__doc__r†   r(   Úrer@   r&   Úcollections.abcr   r   Úhermes_cli.configr   Ú!GATEWAY_SERVICE_RESTART_EXIT_CODEr   rž   r   rp   r   rX   r“   Ú,DEFAULT_GATEWAY_POST_INTERRUPT_GRACE_TIMEOUTr�   r�   r—   ÚSYSTEMD_STOP_HEADROOM_SÚSYSTEMD_TIMEOUT_STOP_SEC_FLOORÚ LAUNCHD_GUI_EXIT_TIMEOUT_CLAMP_SÚLAUNCHD_STOP_CLEANUP_RESERVE_Sr\   ÚcompileÚ	MULTILINEr   Úobjectr!   r   r.   r0   rF   rR   rY   r^   rq   rh   rk   rs   rv   r{   rM   rˆ   r‹   rŽ   r‘   r”   rš   r¡   r¥   rz   r   r   ú<module>r³      sÚ  ðÙ Hã Û 	Û 	Û Û 
ß -å ,ð %'Ð !ð "$Ð ð°#ð ¸#ó ð" #GÐ á(-¨n¸WÑ.EÐF]Ñ.^Ó(_Ð %Ù16°~ÀiÑ7PÐQqÑ7rÓ1sÐ .Ø/2Ð ,ñ .3°>À'Ñ3JÐKgÑ3hÓ-iÐ *ñ
 &+¨>¸'Ñ+BÐCWÑ+XÓ%YÐ "ð  $Ð ð Ð Ø!%Ð ð $&Ð  Ø!%Ð ð "%Ð à%˜2Ÿ:™:Ð&IÈ2Ï<É<ÓXÐ ð!¨Vð !¸À¹ó !ñ 7¨3°¨8Ñ#4°tÑ#;ð ÀsÈTÁzó ð( ð$3ð )-ØØ=G¿^¹^ò$3Ø�‰:ð$3ð �S˜#�XÑ Ñ%ð$3ð 
ˆt‰ð	$3ð
 
Ð9Ñ	:ð$3ð ˆT�\ó$3ðV  >ò	FØðFà! D™LðFð ð	Fð
 óFð2a¨ð a°Eó að ¨&ð À%ð ÈEó ò& %€ðN¨3°©:ð N¸$ó Nñ a¨7°3¸°8Ñ+<¸tÑ+Cð aÈtó añ2¨'°#°s°(Ñ*;¸dÑ*Bð 2Èdó 2ðQ dó Qñ�Fð  eð °eó ð PUò 	O Vð 	O°eð 	OÈð 	OÐY^ó 	Oð[ Vð [°ó [ð
X¨&ð X°Uó Xð
P &ð P¨Uó Pði¨fð i¸ó ið adØ;ò+Øð+Ø.3ð+ØHMð+ØX]ð+àð+ð ó+ð& 7YðWà;ÐQhØ3òWØðWØ.3ðWàðWàINðWð ðWð 	ó	Wð" lpò W°Eð WÈuð WÐchð WÐtyô Wr   