
    sjW                    t   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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  ej        e          Zg dZdZdZ G d	 d
e          Z edd           G d d                      Zd>dZd?dZd@dZdddAdZdBd"ZdCd%Z dDd)Z!d*ddd+dEd1Z"d*dd2dFd7Z#dd8dGd=Z$dS )Hu  Unified deadline layer — one bounded-execution primitive, one timeout resolver.

Phase 1 of the architectural fix for the timeout/hang backlog
(https://github.com/NousResearch/hermes-agent/issues/85125).

The tree currently carries at least six site-local deadline mechanisms, each
built for one incident, none shared (tool_executor batch deadline, telegram
``_await_with_thread_deadline``, gateway turn lease, reasoning stale floors,
``human_wait_ceiling``, per-MCP-handler timeouts).  Every new stall report
grows that list by one.  This module is the shared foundation the call sites
migrate onto in later phases:

* :func:`resolve_timeout` — one config-first resolution path for timeout
  values (``timeouts:`` section in config.yaml > legacy env var > default),
  so new surfaces stop inventing ``HERMES_*_TIMEOUT`` env vars (".env is for
  secrets only") and hardcoded literals stop ignoring user config
  (#63302, #53161, #43272 class).

* :func:`clamp_timeout` — platform-safe clamping.  Large user-supplied
  timeouts overflow ``time_t`` inside ``threading.Lock.acquire(timeout=...)``
  / ``Thread.join(timeout=...)`` on macOS and kill whole tool batches
  (#83220).  Clamping at the shared boundary fixes that class once, for
  every consumer.

* :func:`run_bounded_async` — a wall-clock deadline for awaitables that does
  NOT depend on event-loop timers.  ``asyncio.wait_for`` schedules its expiry
  on the loop; when the loop thread itself is blocked in a synchronous call
  (family A of the #84047 stall triage), every asyncio-based timeout in the
  process is silently disabled.  This helper drives the deadline from a
  daemon ``threading.Timer`` (generalizing the proven telegram-adapter
  primitive) and abandons cancellation-shielded tasks instead of waiting for
  cancellation to complete.  The telegram adapter's private copy
  (``plugins/platforms/telegram/adapter.py:_await_with_thread_deadline``)
  migrates onto this in Phase 2 of #85125 — do not let the two drift in the
  meantime; fix bugs here first.

* :func:`run_bounded_sync` — the same contract for synchronous callables
  bounded from a synchronous context (daemon worker thread, abandoned on
  expiry).

* :func:`kill_process_tree` — portable whole-tree termination so
  kill-on-timeout stops orphaning descendants (#71148, #59549, #84967,
  #68139 class).  Existing site-local tree-kills that migrate onto this in
  Phase 4 of #85125: ``gateway/status.py`` (taskkill wrapper + psutil
  snapshot/reap pair) and ``tools/code_execution_tool.py`` (psutil
  recursive children kill).

Design invariants:

* Exceptions raised by the bounded operation propagate unchanged — callers
  keep their existing error handling.  Only the *timeout* outcome is
  reified (as :class:`BoundedResult`), because that is the outcome the
  call sites keep getting wrong.
* A timeout produced by this layer is OUR deadline, not the provider's.
  Callers that feed errors into ``agent/error_classifier.py`` should
  classify :class:`DeadlineExpired` distinctly from transport timeouts
  (the #59549 / #80323 misattribution class).
* ``None`` timeout means unbounded, and non-positive resolved values are
  normalized to ``None`` (matching the existing
  ``HERMES_CONCURRENT_TOOL_TIMEOUT_S`` convention).
    )annotationsN)	dataclass)Any	AwaitableCallableOptional)MAX_SAFE_TIMEOUT_SBoundedResultDeadlineExpiredclamp_timeoutresolve_timeoutrun_bounded_asyncrun_bounded_synckill_process_treeg    8~Ag      @c                  $     e Zd ZdZd fdZ xZS )r   uN  A deadline enforced by this layer expired.

    Distinct from transport/provider timeout types on purpose: when this is
    raised (or a :class:`BoundedResult` reports ``timed_out``), the timeout
    was Hermes's own bound — error classification must not attribute it to
    the provider (#59549 / #80323 misattribution class).
    labelstr	timeout_sfloatc                t    t                                          d|dd|            || _        || _        d S )Nzdeadline expired after z.1fzs: )super__init__r   r   )selfr   r   	__class__s      2/home/agent/.hermes/hermes-agent/agent/deadline.pyr   zDeadlineExpired.__init__q   sA    L9LLLULLMMM
"    )r   r   r   r   )__name__
__module____qualname____doc__r   __classcell__)r   s   @r   r   r   h   sG         # # # # # # # # # #r   r   T)frozenkw_onlyc                  N    e Zd ZU dZded<   ded<   ded<   ded	<   d
ed<   ddZdS )r
   u   Outcome of a bounded operation.

    ``timed_out`` is the reified outcome; on completion ``value`` holds the
    operation's return value.  Operation exceptions are never captured here —
    they propagate to the caller unchanged.
    bool	timed_outr   valuer   	elapsed_sOptional[float]r   r   r   returnc                p    | j         r)t          | j        t          | j        pd                    | j        S )z>Return ``value``, raising :class:`DeadlineExpired` on timeout.g        )r&   r   r   r   r   r'   )r   s    r   raise_if_timed_outz BoundedResult.raise_if_timed_out   s6    > 	L!$*eDN4Ic.J.JKKKzr   N)r*   r   )r   r   r   r    __annotations__r,    r   r   r
   r
   w   si           OOOJJJJJJ     r   r
   timeoutr)   r*   c                   | dS 	 t          |           }n3# t          t          f$ r t                              d|            Y dS w xY w||k    rt                              d           dS |dk    rdS t          |t                    S )u;  Normalize a timeout value for platform wait primitives.

    * ``None`` stays ``None`` (unbounded).
    * Non-positive values become ``None`` (unbounded) — matching the existing
      ``HERMES_CONCURRENT_TOOL_TIMEOUT_S`` "0 disables the bound" convention.
    * Values above :data:`MAX_SAFE_TIMEOUT_S` are capped so they can never
      overflow ``time_t`` inside ``Lock.acquire`` / ``Thread.join`` on macOS
      (#83220).
    * Non-numeric values are treated as unset (``None``) with a warning
      rather than crashing the call path they were meant to protect.
    Nz<clamp_timeout: non-numeric timeout %r; treating as unboundedz1clamp_timeout: NaN timeout; treating as unboundedr   )r   	TypeError
ValueErrorloggerwarningminr	   )r/   r'   s     r   r   r      s     tgz"   UW^___tt ~~JKKKtzztu()))s    ,AAdictc                     	 ddl m}   |                                 d          }t          |t                    r|ni S # t
          $ r! t                              dd           i cY S w xY w)zRead the ``timeouts:`` root section from config.yaml (read-only).

    Isolated for testability and so a broken config read can never take down
    the call path the timeout was protecting.
    r   )load_config_readonlytimeoutsz,timeouts: config read failed; using defaultsTexc_info)hermes_cli.configr8   get
isinstancer6   	Exceptionr3   debug)r8   sections     r   _timeouts_sectionrB      s    ::::::&&((,,Z88$Wd33;ww;   CdSSS			s   ;> (A)(A)rA   keyr   r   c                    | }|                     d          D ]&}t          |t                    r||vr dS ||         }'|S )z=Walk ``a.b.c`` through nested dicts; return None when absent..N)splitr>   r6   )rA   rC   nodeparts       r   _lookup_dottedrI      sS    D		#  $%% 	T)9)944DzKr   )env_vardefaultrJ   Optional[str]c                   t          t                      |           }|nt          |t                    s=	 t	          |          }||k    rt          |          S n# t          t          f$ r Y nw xY wt          	                    d| |           |rrt          j        |d                                          }|rI	 t          t	          |                    S # t          $ r t          	                    d||           Y nw xY wt          |          S )u[  Resolve a timeout in seconds for a dotted config key.

    Precedence (established by the ``providers.*.request_timeout_seconds``
    pattern — config wins over the legacy env var):

    1. ``timeouts.<key>`` in config.yaml (dotted key walks nested maps, e.g.
       ``tools.concurrent_batch`` reads ``timeouts: {tools: {concurrent_batch: ...}}``)
    2. ``env_var`` when set and non-empty (legacy bridge — internal mechanism
       and back-compat only; new surfaces must not grow new user-facing
       ``HERMES_*`` timeout env vars)
    3. ``default``

    The winning value is passed through :func:`clamp_timeout`, so ``0`` or a
    negative value means "unbounded" and oversized values are made
    platform-safe.  Invalid (non-numeric) config/env values fall through to
    the next source with a warning instead of breaking the protected path.
    Nz6timeouts.%s: invalid value %r in config.yaml; ignoring zinvalid %s=%r; ignoring)rI   rB   r>   r%   r   r   r1   r2   r3   r4   osgetenvstrip)rC   rK   rJ   rawr'   env_raws         r   r   r      s2   . *,,c
2
2C
 #t$$ 	c

E>>(/// "z*   OQTVYZZZ L)GR((..00 	LL$U7^^444 L L L8'7KKKKKL !!!s#   #A A.-A.9C &C>=C>task'asyncio.Future[Any]'Nonec                ~    	 |                                  s|                                  dS dS # t          $ r Y dS w xY w)zGObserve an abandoned task's outcome so it never logs 'never retrieved'.N)	cancelled	exceptionr?   )rT   s    r   _consume_abandonedrZ     s[    ~~ 	NN	 	   s   (. 
<<
on_abandonCallable[[], Awaitable[Any]]c                   K   	  |              d{V  dS # t           $ r  t                              dd           Y dS w xY w)zGRun abandonment cleanup fully fire-and-forget (its failures swallowed).Nzdeadline abandon-cleanup failedTr:   )r?   r3   r@   )r[   s    r   _run_abandon_cleanupr^     so      Gjll G G G6FFFFFFGs    &A A r   r   r   c                    t                               d| |t                     	 t          j        d           d S # t
          $ r  t                               dd           Y d S w xY w)Nu  [deadline] %r deadline (%.0fs) expired but the event loop has not processed the expiry after a further %.0fs — the loop thread appears BLOCKED in a synchronous call, which is why no asyncio timeout can fire. Dumping all thread stacks to stderr to identify the blocking frame.T)all_threadsz"faulthandler traceback dump failedr:   )r3   r4   _LOOP_BLOCKED_DUMP_GRACE_Sfaulthandlerdump_tracebackr?   r@   )r   r   s     r   _dump_blocked_loop_diagnosticsrd     s    
NN	
 	"	 	 	J#555555 J J J9DIIIIIIJs   ; &A%$A%	operation)r   r[   dump_on_blocked_loop	awaitableAwaitable[Any]&Optional[Callable[[], Awaitable[Any]]]rf   r%   c                 K   t          |          t          j                    }0|  d{V }t          d|t          j                    |z
  d          S t	          j        |           }t	          j                                                    t          j	                    dfddfd}dfd}	t          j
        |          }
d	|
_        |
                                 d}|r8t          j
        t          z   |	          }d	|_        |                                 	 	 t	          j        |ht          j        
           d{V \  }}nB# t          j        $ r0 |                                 |                    t&                      w xY w||v r                                s                                 | d{V }t          d|t          j                    |z
            |
                                 ||                                                                  S |                                 |                    t&                     |;t	          j        t-          |                    }|                    t&                     t.                              d           t          d	dt          j                    |z
            |
                                 ||                                                                  S # |
                                 ||                                                                  w xY w)u>  Await ``awaitable`` under a wall-clock deadline independent of loop timers.

    On completion returns ``BoundedResult(timed_out=False, value=...)``;
    exceptions from the operation (including ``asyncio.CancelledError`` from a
    caller cancelling *us*) propagate unchanged.

    On timeout the underlying task is cancelled and **abandoned** — we do not
    await cancellation completion, because cancellation-shielded scopes (anyio,
    httpcore init, MCP SDK teardown) are exactly the paths that wedge forever.
    ``on_abandon`` (zero-arg callable returning an awaitable) is scheduled as
    detached best-effort cleanup for the half-built state the abandoned task
    may leave behind.  Returns ``BoundedResult(timed_out=True, value=None)``.

    ``timeout=None`` (or a non-positive resolved value) awaits unbounded.
    NFr&   r'   r(   r   r   r*   rV   c                                                                                        s                     d            d S d S N)setdone
set_result)deadlineloop_processed_expirys   r   _mark_expiredz(run_bounded_async.<locals>._mark_expiredG  sI    !!###}} 	&%%%%%	& 	&r   c                 2                                     d S rm   )call_soon_threadsafe)rs   loops   r   _expire_from_threadz.run_bounded_async.<locals>._expire_from_threadL  s    !!-00000r   c                 T                                     st                      d S d S rm   )is_setrd   )r   rr   r   s   r   _watchdog_checkz*run_bounded_async.<locals>._watchdog_checkO  s7    $++-- 	=*5)<<<<<	= 	=r   T)return_whenz3[deadline] %r timed out after %.1fs; task abandonedr*   rV   )r   time	monotonicr
   asyncioensure_futureget_running_loopcreate_future	threadingEventTimerdaemonstartra   waitFIRST_COMPLETEDCancelledErrorcanceladd_done_callbackrZ   ro   rn   r^   r3   r4   )rg   r/   r   r[   rf   r   r'   rT   rw   rz   timerwatchdogro   _cleanuprs   rq   rv   rr   r   s     `            @@@@@r   r   r   %  s     . g&&INEuET^EUEUX]E]imuz{{{{ ++D#%%D'+'9'9';';H%O--& & & & & & &
1 1 1 1 1 1 1= = = = = = = = OI':;;EEL	KKMMM*.H ?22O
 
 !$	#Lx g.E        GD!! % 	 	 	
 KKMMM""#5666	 4<<==?? "!!!JJJJJJE 5IYIY\aIamv  D  E  E  E 	OO 	!!#### 	1222!+,@,L,LMMG%%&8999LeU^___t44>CSCSV[C[gpx}~~~OO 	!!#### 	OO 	!!####s'   /+E L$ ?FAL$ 7B.L$ $A M$)r   
on_timeoutfnCallable[[], Any]r   Optional[Callable[[], None]]c                  	 t          |          }t          j                    }|0t          d              t          j                    |z
  d|          S i t	          j                    	d	 fd}t	          j        |d| d	          }|                                 	                    |          s~t          
                    d
||           |8	  |             n,# t          $ r t                              dd           Y nw xY wt          ddt          j                    |z
  ||          S dv rd         t          d                    d          t          j                    |z
  ||          S )u  Run ``fn`` in a daemon worker thread under a wall-clock deadline.

    On completion returns its value (exceptions re-raised in the caller).
    On expiry the worker thread is **abandoned** (daemon, so it cannot block
    interpreter exit), ``on_timeout`` (if given) runs best-effort in the
    caller's thread — e.g. to mark a backend suspect or kill a subprocess —
    and ``BoundedResult(timed_out=True)`` is returned.

    Intended for infrequent, seconds-scale blocking backend calls. Do NOT
    use per-item in hot loops: each call spawns a thread, and every timeout
    permanently leaks an abandoned daemon thread — a wedged backend called
    in a retry loop would accumulate them.

    ``timeout=None`` (or non-positive) blocks until ``fn`` returns.
    NFrk   r*   rV   c                     	              d<   n# t           $ r} | d<   Y d } ~ nd } ~ ww xY w                                 d S #                                  w xY w)Nr'   exc)BaseExceptionrn   )r   boxro   r   s    r   _workerz!run_bounded_sync.<locals>._worker  sp    	244CLL 	 	 	CJJJJJJ	 HHJJJJJDHHJJJJs%    A 
*%A *A Az	deadline-T)targetnamer   z5[deadline] %r timed out after %.1fs; worker abandonedz#deadline on_timeout callback failedr:   r   r'   r|   )r   r}   r~   r
   r   r   Threadr   r   r3   r4   r?   r@   r=   )
r   r/   r   r   r   r   r   threadr   ro   s
   `       @@r   r   r     s   , g&&INEuBBDDDNDTDTW\D\hltyzzzzC?D        000  F LLNNN99Y NPUW`aaa!S
 S S SBTRRRRRSt44>CSCSV[C[gpx}~~~~||%j50@0@DNL\L\_dLdpy  BG  H  H  H  Hs   
C" "&D
D)sigpidintr   Optional[int]c          	        t           j        dk    r	 ddlm}  |            }n# t          $ r d}Y nw xY w	 t          j        ddddt          |           gdd	d
|          }|j        dk    S # t          $ r! t          
                    d| d           Y d
S w xY wddl}||j        }g }	 ddl}|                    t          |                                         d          }n# t          $ r g }Y nw xY wd
}	 t#          j        |           }	n# t&          t(          t*          f$ r d}	Y nw xY w	 |	|	| k    rt#          j        |	|           nt#          j        | |           d}n?# t&          $ r Y n3t(          t*          f$ r  t          
                    d| d           Y nw xY w|D ]>}
	 |
                                r|
                    |           d}/# t          $ r Y ;w xY w|S )uY  Terminate ``pid`` and all its descendants, portably.

    Kill-on-timeout that signals only the direct child orphans process trees
    (cron scripts, in-container shells, browser daemons — #71148 class).

    * Windows: ``taskkill /F /T`` terminates the tree (``sig`` ignored;
      Windows has no equivalent). Console-window flash is suppressed via
      ``windows_hide_flags`` and the exit code is checked, so a dead or
      inaccessible PID reports ``False`` like the POSIX path.
    * POSIX: the descendant set is snapshotted via psutil (a hard
      dependency) BEFORE any signal — once the parent dies its children are
      reparented and can no longer be found by a parent walk. Then the
      process group is signalled when ``pid`` leads one (covers
      grandchildren in the same session in one syscall), and every
      snapshotted descendant is signalled individually — which also reaches
      descendants that created their OWN sessions (a child that called
      ``setsid``, exactly what user shell commands do; see
      tools/environments/base.py). ``sig`` defaults to ``SIGKILL``.
      psutil's identity-aware ``Process`` (PID + create time) means a
      recycled PID is never signalled.

    Returns True when the target (or any of its tree) was signalled, False
    when the process was already gone or every termination call failed.
    win32r   )windows_hide_flagstaskkillz/Fz/Tz/PIDT   F)capture_outputr/   checkcreationflagsz-kill_process_tree: taskkill failed for pid %sr:   N)	recursivez+kill_process_tree: signal failed for pid %s)sysplatformhermes_cli._subprocess_compatr   r?   
subprocessrunr   
returncoder3   r@   signalSIGKILLpsutilProcessr   childrenrO   getpgidProcessLookupErrorPermissionErrorOSErrorkillpgkill
is_runningsend_signal)r   r   r   r   proc_signaldescendantsr   	signalledpgidchilds              r   r   r     s   2 |w	HHHHHH..00MM 	 	 	MMM		>T4S:#+  D ?a'' 	 	 	LLH#X\L]]]55	 
{o KnnSXX..77$7GG    
 I z#9   X IdC    GC		   W% X X XBCRVWWWWWX
   	!! !!!#&&& 	 	 	 	H	sk   # 225A, ,'BB*:C% %C43C4:D D+*D+/5E% %
F!1-F! F!)+G
G"!G")r/   r)   r*   r)   )r*   r6   )rA   r6   rC   r   r*   r   )rC   r   rK   r)   rJ   rL   r*   r)   )rT   rU   r*   rV   )r[   r\   r*   rV   )r   r   r   r   r*   rV   )rg   rh   r/   r)   r   r   r[   ri   rf   r%   r*   r
   )
r   r   r/   r)   r   r   r   r   r*   r
   )r   r   r   r   r*   r%   )%r    
__future__r   r   rb   loggingrO   r   r   r   r}   dataclassesr   typingr   r   r   r   	getLoggerr   r3   __all__r	   ra   TimeoutErrorr   r
   r   rB   rI   r   rZ   r^   rd   r   r   r   r.   r   r   <module>r      s  < <| # " " " " "       				     



      ! ! ! ! ! ! 5 5 5 5 5 5 5 5 5 5 5 5		8	$	$	 	 	& "  ! # # # # #l # # # $%%%       &%** * * *@        "	/" /" /" /" /" /"x   G G G GJ J J J* 9=!%Y$ Y$ Y$ Y$ Y$ Y$H /35H 5H 5H 5H 5H 5Hx 9= _ _ _ _ _ _ _ _r   