
    epj                    p   U 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	m
Z
 ddlmZ ddlmZmZmZmZmZmZ ddlmZ ddlmZ dd	lmZ  ej        e          ZeZdad
ed<    ej                     Z!da"ded<    ej                     Z#i Z$ded<   dZ%dZ&dZ'dZ(dZ) ej                     Z*dZ+dZ,dZ-dZ. ej                     Z/da0ded<    ej1                    Z2d Z3ddZ4dd!Z5edd#            Z6dd&Z7dd)Z8dd*Z9dd-Z:dd.Z;dd/Z<dd0Z=dd2Z>dd4Z?dd8Z@dd9ZAdd:ZBdd;ZCdd<ZDdd=ZEdd?ZFddBZGddCZHddEZIddFZJdGdGdGdHddKZK	 	 	 dddLZLddMZMddNZNddOZOddGdGde%ddPdd_ZPddaZQddcZRdddZSddeZTddGdGde%dddfddiZUddkZVddmZWddnZXddoZYddpZZddvZ[ddxZ\ddd{Z]	 	 	 	 ddd}Z^dd~Z_dS )u  
Async (background) delegation registry.

Backs ``delegate_task(background=true)``: the parent agent dispatches a
subagent that runs on a module-level daemon executor and returns a handle
immediately, so the user and the model can keep working while the child runs.

When the child finishes, a completion event is pushed onto the SHARED
``process_registry.completion_queue`` with ``type="async_delegation"``. The
CLI (``cli.py`` process_loop) and gateway (``_run_process_watcher`` /
``completion_queue`` drain) already poll that queue while the agent is idle
and forge a fresh user/internal turn from each event. We deliberately reuse
that rail rather than reaching into a running agent loop:

  - completions surface as a NEW turn when the agent is idle, never spliced
    between a tool result and an assistant message. That keeps strict
    message-role alternation legal and the prompt cache intact (hard
    invariant: never mutate past context).
  - we inherit the queue's de-dup, crash-recovery checkpoint, and the
    existing CLI + gateway drain wiring for free — no new drain loops in the
    two largest files in the repo.

The completion payload carries a RICH, self-contained task-source block (the
original goal, the context the parent supplied, toolsets, model, dispatch
time, status, and the full result summary). When the result re-enters the
conversation the parent may be deep in unrelated context and won't remember
why the subagent existed; the block lets it either use the result or
re-dispatch if the world has moved on.

This module owns ONLY the async lifecycle. The actual child build + run is
delegated back to ``delegate_tool._run_single_child`` via an injected
runner, so all the credential leasing, heartbeat, timeout, and result-shaping
logic stays in one place.
    )annotationsN)ThreadPoolExecutor)contextmanager)AnyCallableDictIteratorListOptionalget_hermes_home)DaemonThreadPoolExecutor)propagate_context_to_threadzOptional[ThreadPoolExecutor]	_executorint_executor_max_workerszDict[str, Dict[str, Any]]_records   2   i:	 i     g      >@g      |@g     @g      ^@zOptional[threading.Thread]_monitor_threadc                 $    t                      dz  S )Nzstate.dbr        </home/thesage/.hermes/hermes-agent/tools/async_delegation.py_db_pathr   w   s    z))r   returnsqlite3.Connectionc                     t                      } | j                            dd           t          j        | d          }	 t          |           n## t          $ r |                                  w xY w|S )NT)parentsexist_ok
   timeout)r   parentmkdirsqlite3connect_initialize_schema	Exceptionclose)pathconns     r   _connectr.   {   s    ::DKdT222?4,,,D4        	

	
 Ks   A  A2r-   Nonec                    ddl m}  || d           |                     d           d |                     d          D             }dD ]$\  }}||vr|                     d	| d
|            %d S )Nr   )apply_wal_with_fallbackzstate.db (async_delegation))db_labela+  CREATE TABLE IF NOT EXISTS async_delegations (
            delegation_id TEXT PRIMARY KEY,
            origin_session TEXT NOT NULL,
            origin_ui_session_id TEXT NOT NULL DEFAULT '',
            parent_session_id TEXT,
            state TEXT NOT NULL,
            dispatched_at REAL NOT NULL,
            completed_at REAL,
            updated_at REAL NOT NULL,
            event_json TEXT,
            result_json TEXT,
            delivery_state TEXT NOT NULL DEFAULT 'pending',
            delivery_attempts INTEGER NOT NULL DEFAULT 0,
            delivered_at REAL,
            owner_pid INTEGER,
            owner_started_at INTEGER,
            task_json TEXT,
            delivery_claim TEXT,
            delivery_claimed_at REAL,
            origin_session_id TEXT NOT NULL DEFAULT ''
        )c                    h | ]
}|d          S )   r   ).0rows     r   	<setcomp>z%_initialize_schema.<locals>.<setcomp>   s    VVV#s1vVVVr   z$PRAGMA table_info(async_delegations)))	owner_pidINTEGER)owner_started_atr9   )	task_jsonTEXT)delivery_claimr<   )delivery_claimed_atREAL)origin_session_idr<   z)ALTER TABLE async_delegations ADD COLUMN  )hermes_stater1   execute)r-   r1   columnsnamesql_types        r   r)   r)      s    444444D+HIIIILL	  . WV.T!U!UVVVG X Xh wLLVTVVHVVWWWX Xr   Iterator[sqlite3.Connection]c               #     K   t                      } 	 | 5  | V  ddd           n# 1 swxY w Y   |                                  dS # |                                  w xY w)u  Open a connection, commit/rollback on exit, and ALWAYS close it.

    ``sqlite3.Connection.__enter__``/``__exit__`` only commit or roll back the
    transaction; they do not close the connection. Using ``with _connect()``
    alone therefore leaks a connection — and its WAL/SHM file descriptors — on
    every durable dispatch, completion, and delivery-claim, deferring the close
    to the garbage collector. On a long-running gateway that exhausts
    ``RLIMIT_NOFILE`` (the cron-ledger sibling of this bug was #69567 / PR #69594).
    N)r.   r+   )r-   s    r   _transactionrI      s       ::D 	 	JJJ	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	



s&   A %A )A )A ArecordDict[str, Any]c                    t          j                     }	 ddlm}  |t          d                                                    }n# t
          $ r d }Y nw xY w fddD             }t          5  t                      5 }|                    d d          	                    dd	           	                    d
d	           	                    d           d         |t          d                                          |t          j        |           	                    dd	          f
           d d d            n# 1 swxY w Y   d d d            n# 1 swxY w Y   t                       d S )Nr   )get_process_start_timeosc                D    i | ]}|v |                     |          S r   get)r5   keyrJ   s     r   
<dictcomp>z%_persist_dispatch.<locals>.<dictcomp>   s6       &== 	VZZ__==r   )goalgoalscontexttoolsetsrolemodelis_batcha~  INSERT OR REPLACE INTO async_delegations
               (delegation_id, origin_session, origin_ui_session_id,
                parent_session_id, state, dispatched_at, updated_at,
                delivery_state, delivery_attempts, owner_pid,
                owner_started_at, task_json, origin_session_id)
               VALUES (?, ?, ?, ?, 'running', ?, ?, 'pending', 0, ?, ?, ?, ?)delegation_idsession_key origin_ui_session_idparent_session_iddispatched_atr@   )timegateway.statusrM   
__import__getpidr*   _DB_LOCKrI   rC   rQ   jsondumps_prune_durable_records)rJ   nowrM   r:   task_payloadr-   s   `     r   _persist_dispatchrk      s   
)++C 99999911*T2B2B2I2I2K2KLL          X  L
 
 
 
<>> 
TQ O$fjj&C&CZZ.33VZZ@S5T5TO$c:d+;+;+B+B+D+Dtz,77ZZ+R00	2	
 	
 	

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 sH   0A AA.E=B-D6*E6D:	:E=D:	>EEEr[   strc                    t           5  t                      5 }|                    d| f           d d d            n# 1 swxY w Y   d d d            d S # 1 swxY w Y   d S )Nz3DELETE FROM async_delegations WHERE delegation_id=?)re   rI   rC   r[   r-   s     r   _delete_durable_delegationro      s   	 ^ ^<>> ^TJ]L\]]]^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^s.   A;A?	A?	AAAc                 r   t          j                     } | t          z
  }t          5  t                      5 }|                    d|f           |                    d                                          d         }t          d|t          z
            }|r|                    d|f           |                    d                                          d         }t          d|t          z
            }|r|                    d|f           ddd           n# 1 swxY w Y   ddd           dS # 1 swxY w Y   dS )zBBound terminal history, preferring delivered records for deletion.zQDELETE FROM async_delegations WHERE delivery_state='delivered' AND updated_at < ?zRSELECT COUNT(*) FROM async_delegations WHERE state NOT IN ('running','finalizing')r   aX  DELETE FROM async_delegations WHERE delegation_id IN (
                     SELECT delegation_id FROM async_delegations
                     WHERE state NOT IN ('running','finalizing')
                     ORDER BY CASE delivery_state WHEN 'delivered' THEN 0 ELSE 1 END,
                              updated_at ASC LIMIT ?
                   )z~SELECT COUNT(*) FROM async_delegations
               WHERE state NOT IN ('running','finalizing') AND delivery_state='pending'a  DELETE FROM async_delegations WHERE delegation_id IN (
                     SELECT delegation_id FROM async_delegations
                     WHERE state NOT IN ('running','finalizing') AND delivery_state='pending'
                     ORDER BY updated_at ASC LIMIT ?
                   )N)	ra   _DURABLE_RETENTION_SECONDSre   rI   rC   fetchonemax_MAX_RETAINED_COMPLETED_MAX_DURABLE_PENDING)ri   cutoffr-   terminal_countexcesspending_countoverflows          r   rh   rh      s   
)++C--F	    <>>  T_I	
 	
 	
 `
 

(**Q Q)@@AA 		LL 	   [
 
 (**Q q-*>>?? 	LL
   3                                                                 s5   D,CDD,D	D,D	D,,D03D0eventresultc                   t          j                     }t          5  t                      5 }|                    d|                     dd          |                     d|          |t          j        |           t          j        |          | d         f           d d d            n# 1 swxY w Y   d d d            d S # 1 swxY w Y   d S )NzUPDATE async_delegations SET state=?, completed_at=?, updated_at=?,
               event_json=?, result_json=?, delivery_state='pending'
               WHERE delegation_id=?status	completedcompleted_atr[   )ra   re   rI   rC   rQ   rf   rg   )r{   r|   ri   r-   s       r   _persist_completionr     s8   
)++C	 
 
<>> 
T( YYx--uyy/M/MsZ
6 2 2E/4JL		
 	
 	

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
s5   B=A/B%B=%B)	)B=,B)	-B==CCc                    t           5  t                      5 }|                    dt          j                    | f           d d d            n# 1 swxY w Y   d d d            d S # 1 swxY w Y   d S )NzfUPDATE async_delegations SET delivery_attempts=delivery_attempts+1, updated_at=? WHERE delegation_id=?)re   rI   rC   ra   rn   s     r   _note_delivery_attemptr     s    	 
 
<>> 
TtY[[-(	
 	
 	

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
s4   A%*AA%A	A%A	A%%A),A)c                 f   	 ddl m} m} n# t          $ r Y dS w xY wt	          j                    }d}t
          5  t                      5 }|                    d                                          }|D ]}|\	  }}}	}
}}}}}d}|rE | t          |                    }|r+|) |t          |                    t          |          k    }|r[t          j        |pd          }i ddd	|d
|d|	d|pdd|
d|                    dd          d|                    d          d|                    d          d|                    d          d|                    d          d|                    d          dt          |                    d                    ddddddd|d|i}dd|d         d}|                    d||t          j        |          t          j        |          |f           |dz  }	 ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   |S ) zEClassify records whose owning process disappeared as outcome unknown.r   )_pid_existsrM   a  SELECT delegation_id, origin_session, origin_ui_session_id,
                      parent_session_id, dispatched_at, owner_pid,
                      owner_started_at, task_json, origin_session_id
               FROM async_delegations WHERE state IN ('running','finalizing')FNz{}typeasync_delegationr[   r\   r^   r@   r]   r_   rT   rU   rV   rW   rX   rY   rZ   r~   unknownsummaryerrorzLDelegation owner exited before recording a terminal result; outcome unknown.r`   r   )r~   r   r   zUPDATE async_delegations SET state='unknown', completed_at=?,
                   updated_at=?, event_json=?, result_json=?, delivery_state='pending'
                   WHERE delegation_id=?r4   )rb   r   rM   r*   ra   re   rI   rC   fetchallr   rf   loadsrQ   boolrg   )r   rM   ri   	recoveredr-   rowsr6   r[   r\   	origin_ui	parent_idr`   pidstartedr;   r@   livetaskr{   r|   s                       r   recover_abandoned_delegationsr   %  s   FFFFFFFFF   qq
)++CI	 ' '<>> 'T||Q
 

 (** 	   	  	C;>9]KI}'9&7D L"{3s88,, LG/11#c((;;s7||KD :i/400D*,;]{,BI
 $%6%<" $Y 178L8L '** -6txx	7J7J DHHZ00 39$((6:J:J '** -7TXXj=Q=Q8R8R ) &/ g   1? E !*dU7^TTFLL, c4:e,,dj.@.@-P	   NIIA 	' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' 'P sB    
H&F;HH&H	H&H	H&&H*-H*c                   t                       t          5  t                      5 }|                    d                                          }|D ]H\  }}t          j        |          }t          |t                    rd|d<   | 	                    |           I	 ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   t          |          S )uz  Enqueue durable pending completions as fresh turns after process start.

    Every restored event is stamped ``restored=True`` (in-memory only — the
    stamp is added after the durable payload is deserialized and is never
    persisted). Restored events originate from a *previous* process, so no
    consumer in THIS process implicitly owns them: drain paths that run
    without an ownership filter (the legacy single-session behavior) must
    leave them queued for a consumer that can positively prove ownership,
    otherwise a brand-new session adopts a dead session's delegation
    results seconds after boot (#64484).
    zSELECT delegation_id, event_json FROM async_delegations
               WHERE state != 'running' AND delivery_state='pending' AND event_json IS NOT NULL
               ORDER BY completed_at, delegation_idTrestoredN)r   re   rI   rC   r   rf   r   
isinstancedictputlen)target_queuer-   r   _delegation_idpayloadevts         r   restore_undelivered_completionsr   X  sV    "###	 
" 
"<>> 
"T||7
 
 (**	 	
 (, 	" 	"#NG*W%%C#t$$ '"&JS!!!!		"
" 
" 
" 
" 
" 
" 
" 
" 
" 
" 
" 
" 
" 
" 
" 
" 
" 
" 
" 
" 
" 
" 
" 
" 
" 
" 
" 
" 
" 
" t99s5   B<A3B%B<%B)	)B<,B)	-B<<C C r   c                   t          j                     }t          5  t                      5 }|                    d||| f          }|j        dk    cddd           cddd           S # 1 swxY w Y   ddd           dS # 1 swxY w Y   dS )zDAtomically acknowledge successful injection of a durable completion.zUPDATE async_delegations SET delivery_state='delivered', delivered_at=?, updated_at=?
               WHERE delegation_id=? AND delivery_state!='delivered'r4   Nra   re   rI   rC   rowcount)r[   ri   r-   curs       r   mark_completion_deliveredr   s  s)   
)++C	 ! !<>> !TllH#}%
 

 |q ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !4   A?$A'A?'A+	+A?.A+	/A??BBclaim_idc                   t          j                     }t          5  t                      5 }|                    d| f                                          }|	 ddd           ddd           dS |                    d|||| |dz
  f          }|j        dk    cddd           cddd           S # 1 swxY w Y   ddd           dS # 1 swxY w Y   dS )zBClaim one pending completion across competing consumers/processes.zBSELECT delivery_state FROM async_delegations WHERE delegation_id=?NTa  UPDATE async_delegations SET delivery_claim=?, delivery_claimed_at=?,
                      delivery_attempts=delivery_attempts+1, updated_at=?
               WHERE delegation_id=? AND delivery_state='pending'
                 AND (delivery_claim IS NULL OR delivery_claimed_at < ?)i,  r4   )ra   re   rI   rC   rr   r   )r[   r   ri   r-   r6   r   s         r   claim_completion_deliveryr     s   
)++C	 ! !<>> !TllP
 
 (** 	 ;! ! ! ! ! ! ! ! ! ! ! ! ! ! llL sCc	:
 
 |q ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !s@   C-B0C/(B0C0B4	4C7B4	8CCCr   consumerOptional[str]c                .   |                      d          dk    rdS t          |                      d          pd          }|sdS | dt          d                                           dt	          j                    j         }t          ||          r|ndS )zCClaim a durable delegation event; non-durable events need no token.r   r   r]   r[   :rN   N)rQ   rl   rc   rd   uuiduuid4hexr   )r   r   r[   r   s       r   claim_event_deliveryr     s    
wwv,,,r006B77M rKKZ--4466KK9IKKH0IIS88tSr   c           	        t          j                     }t          5  t                      5 }|                    d|| |t          f          }|j        dk    r:t                              d| t                     	 ddd           ddd           dS |                    d|| |f          }|j        dk    cddd           cddd           S # 1 swxY w Y   ddd           dS # 1 swxY w Y   dS )u  Release a failed delivery claim so another consumer may retry.

    Attempts are counted at claim time, so a row that keeps being claimed and
    released has burned real delivery attempts. Once the budget is exhausted
    the row converges to a terminal ``dropped`` state instead of returning to
    ``pending`` — otherwise an undeliverable completion replays on every
    gateway restart forever (restore_undelivered_completions only restores
    pending rows).
    a	  UPDATE async_delegations SET delivery_state='dropped',
                      delivery_claim=NULL, delivery_claimed_at=NULL, updated_at=?
               WHERE delegation_id=? AND delivery_state='pending'
                 AND delivery_claim=? AND delivery_attempts>=?r4   znAsync delegation %s exhausted its %d delivery attempts; marking terminally dropped (result remains queryable).NTzUPDATE async_delegations SET delivery_claim=NULL,
                      delivery_claimed_at=NULL, updated_at=?
               WHERE delegation_id=? AND delivery_state='pending'
                 AND delivery_claim=?)ra   re   rI   rC   _MAX_DELIVERY_ATTEMPTSr   loggerwarning)r[   r   ri   r-   cappedr   s         r   release_completion_deliveryr     s    )++C	 ! !<>> !TB -+AB
 
 ?aNNI5  
 ! ! ! ! ! ! ! ! ! ! ! ! ! ! ll) -*
 
 |q -! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !sA   C#AC7C##C2C#C	C#C	C##C'*C'c                   t          j                     }t          5  t                      5 }|                    d|| |f          }|j        dk    cddd           cddd           S # 1 swxY w Y   ddd           dS # 1 swxY w Y   dS )u  Terminally drop a claimed completion that can never be delivered.

    Used when the delivery target is permanently gone — the spawning session
    ended at an explicit user boundary (/new, reset) rather than a compression
    rotation. Marking the row ``dropped`` (not ``delivered``) keeps the ack
    honest, and (not ``pending``) keeps restart recovery from replaying a
    completion that will be fail-closed dropped again every time.
    a  UPDATE async_delegations SET delivery_state='dropped',
                      updated_at=?, delivery_claim=NULL,
                      delivery_claimed_at=NULL
               WHERE delegation_id=? AND delivery_state='pending'
                 AND delivery_claim=?r4   Nr   r[   r   ri   r-   r   s        r   drop_completion_deliveryr     s,    )++C	 	! 	!<>> 	!Tll)
 -*
 
 |q 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	!r   c           	        t          j                     }t          5  t                      5 }|                    d||| |f          }|j        dk    cddd           cddd           S # 1 swxY w Y   ddd           dS # 1 swxY w Y   dS )z;Acknowledge acceptance for the consumer holding this claim.a  UPDATE async_delegations SET delivery_state='delivered',
                      delivered_at=?, updated_at=?, delivery_claim=NULL,
                      delivery_claimed_at=NULL
               WHERE delegation_id=? AND delivery_state='pending'
                 AND delivery_claim=?r4   Nr   r   s        r   complete_completion_deliveryr     s,   
)++C	 	! 	!<>> 	!Tll)
 #}h/
 
 |q 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	!s4   B %A(B (A,	,B /A,	0B  BBc                    |rM|                      d          dk    r6t          t          |                      d          pd          |           d S d S d S Nr   r   r[   r]   )rQ   r   rl   r   r   s     r   complete_event_deliveryr     sa     TCGGFOO'999$S)A)A)GR%H%H(SSSSST T99r   c                    |rM|                      d          dk    r6t          t          |                      d          pd          |           d S d S d S r   )rQ   r   rl   r   s     r   release_event_deliveryr     sa     SCGGFOO'999#C(@(@(FB$G$GRRRRRS S99r   Optional[Dict[str, Any]]c           
        t           5  t                      5 }|                    d| f                                          }d d d            n# 1 swxY w Y   d d d            n# 1 swxY w Y   |d S | |d         |d         |d         |d         |d         rt	          j        |d                   nd |d         |d         |d	         pd
d	S )NzSELECT origin_session, state, dispatched_at, completed_at,
                      result_json, delivery_state, delivery_attempts,
                      origin_session_id
               FROM async_delegations WHERE delegation_id=?r   r4      r               r]   )	r[   origin_sessionstater`   r   r|   delivery_statedelivery_attemptsr@   )re   rI   rC   rr   rf   r   )r[   r-   r6   s      r   get_durable_delegationr     sV   	  <>> Tll? BO@P	
 

 (** 	                              {t&#a&3q6QQ(+A8$*SV$$$Da&s1v V\r  s4   A$*AA$A	A$A	A$$A(+A(max_workersr   c                    t           5  t          | t          k    rt          | d          a| at          cddd           S # 1 swxY w Y   dS )u  Lazily create (or grow) the shared daemon executor.

    We never shrink — ThreadPoolExecutor can't resize — but if the configured
    cap grows between calls we rebuild a larger pool. Existing in-flight
    futures keep running on the old pool until it's garbage collected.
    Nzasync-delegate)r   thread_name_prefix)_executor_lockr   r   _DaemonThreadPoolExecutor)r   s    r   _get_executorr     s     
  .C C C1'#3  I %0!                 s   ,AAAc                     t           5  t          d t                                          D                       cddd           S # 1 swxY w Y   dS )a~  Number of async delegation UNITS currently running.

    A unit is one dispatch: a single subagent OR a whole fan-out batch. A batch
    counts as ONE here because it occupies one async-pool slot (the capacity
    semantics ``dispatch_async_delegation_batch`` relies on). For the count of
    actual concurrent child subagents (batch expanded), use
    ``active_task_count()``.
    c              3  H   K   | ]}|                     d           dv dV  dS )r~   >   runningstalling
finalizingr4   NrP   r5   rs     r   	<genexpr>zactive_count.<locals>.<genexpr>!  sD       
 
uuX"GGG GGGG
 
r   N_records_locksumr   valuesr   r   r   active_countr     s     
 
 
 
 
((
 
 
 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
   0AA	A	r^   c                      sdS t           5  t           fdt                                          D                       cddd           S # 1 swxY w Y   dS )z9Number of live async delegations owned by one UI session.r   c              3     K   | ]E}|                     d           dv r,t          |                     d          pd          k    AdV  FdS )r~   >   r   r   r   r^   r]   r4   N)rQ   rl   )r5   r   r^   s     r   r   z%active_for_session.<locals>.<genexpr>,  sw       
 
uuX"GGGAEE0117R88#$ $ $ $ $ $	
 
r   Nr   )r^   s   `r   active_for_sessionr   '  s     q	 
 
 
 
 
 
__&&
 
 
 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
s   2AAAc                 x   t           5  d} t                                          D ]|}|                    d          dvr|                    d          rH|                    d          }| t	          |t
          t          f          r|rt          |          ndz  } w| dz  } }| cddd           S # 1 swxY w Y   dS )a  Number of async delegation TASKS (child subagents) currently running.

    Unlike ``active_count()`` (units/slots), this expands a batch to its child
    count: a running batch of N tasks contributes N, a single subagent
    contributes 1. This is the truthful "how many subagents are actually
    working right now" figure for observability, where a 3-task batch shown as
    "1" undercounts real concurrent work. Falls back to counting a batch as 1
    if its goal list is missing.
    r   r~   >   r   r   rZ   rU   r4   N)r   r   r   rQ   r   listtupler   )totalr   rU   s      r   active_task_countr   5  s    
 
 
"" 	 	AuuX&???uuZ   gz%$'G'GXEXUWXX

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
s   BB//B36B3r]   r\   r^   r_   r\   r_   c                   |r(t          |                     d          pd          |k    pS|o't          |                     d          pd          |k    p)|o't          |                     d          pd          |k    S )Nr^   r]   r\   r_   )rl   rQ   )rJ   r\   r^   r_   s       r   _matches_session_selectorsr   L  s     
	g#fjj1G&H&H&NB"O"OSg"g 	cOC

= 9 9 ?R@@KO	ca#fjj1D&E&E&K"L"LPa"ar   c                      sssdS t           5  t           fdt                                          D                       cddd           S # 1 swxY w Y   dS )u   Whether a session still owns any live async delegation.

    Live = running / stalling / finalizing — the same states the reapers'
    keepalive treats as active work.
    Fc              3  l   K   | ].}|                     d           dv ot          |          V  /dS )r~   >   r   r   r   r   NrQ   r   r5   r   r^   r_   r\   s     r   r   z'has_live_for_session.<locals>.<genexpr>g  sl       	
 	
  EE(OODD *'%9"3	  	
 	
 	
 	
 	
 	
r   N)r   anyr   r   r   s   ```r   has_live_for_sessionr   Z  s      3 <M u	 

 

 	
 	
 	
 	
 	
 	
 __&&	
 	
 	
 	
 	


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

s   4AAAc                 H    dt          j                    j        d d          S )Ndeleg_r   )r   r   r   r   r   r   _new_delegation_idr   s  s"    *DJLL$RaR(***r   c                 .   d t                                           D             } t          |           t          k    rdS |                     d            | dt          |           t          z
           D ] \  }}t                               |d           !dS )ziDrop the oldest completed records beyond the retention cap.

    Caller must hold ``_records_lock``.
    c                N    g | ]"\  }}|                     d           dk    ||f#S )r~   r   rP   )r5   ridr   s      r   
<listcomp>z+_prune_completed_locked.<locals>.<listcomp>|  s?       C55??i'' 
a'''r   Nc                r    | d                              d          p| d                              d          pdS )Nr4   r   r`   r   rP   )kvs    r   <lambda>z)_prune_completed_locked.<locals>.<lambda>  s1    "Q%))N";";"^r!uyy?Y?Y"^]^ r   )rR   )r   itemsr   rt   sortpop)r   r   _s      r   _prune_completed_lockedr  w  s    
 nn&&  I
 9~~000NN^^N___Fc)nn/FFFG    QS$   r   c                 r    	 ddl m}   | dd          dk    rdS  | dd          pdS # t          $ r Y dS w xY w)u  Raw session id of the ORIGINATING api_server request, or ``""``.

    The obvious source — ``HERMES_SESSION_ID`` via ``get_session_env`` — is
    NOT safe to read at dispatch time: constructing a child agent
    (``agent/agent_init.py``) calls ``set_current_session_id(child.session_id)``,
    clobbering that ContextVar *and* ``os.environ`` with the subagent's
    internal ``{timestamp}_{uuid}`` id moments before the dispatch code reads
    it, so the completion wake would self-post into the subagent's own
    (unread) session instead of the spawner's.

    The request-scoped ``HERMES_SESSION_CHAT_ID`` binding survives child
    construction: ``_bind_api_server_session`` binds ``chat_id`` to the raw
    ``X-Hermes-Session-Id``, and its only writer is ``set_session_vars`` —
    ``set_current_session_id`` never touches it. Gate on the platform: on
    push platforms ``chat_id`` is a chat, not a session, so yield ``""``
    there.
    r   get_session_envHERMES_SESSION_PLATFORMr]   
api_serverHERMES_SESSION_CHAT_ID)gateway.session_contextr  r*   r  s    r   _current_origin_session_idr    sp    $;;;;;;?4b99\II27<<BB   rrs   ( ( 
66)r_   r^   r@   interrupt_fnmax_async_childrenprogress_fnrT   rV   rW   Optional[List[str]]rX   rY   runnerCallable[[], Dict[str, Any]]r@   r  Optional[Callable[[], None]]r  r  Optional[Callable[[], tuple]]c                   t                      t          j                    i dd| d|d|rt          |          ndd|d|d|d	|d
|	d|dddddd|
d|dddddi}t          5  t	          d t
                                          D                       }||k    rdd| ddcddd           S |t
          <   ddd           n# 1 swxY w Y   t          |           t          |          }d$fd}	 |	                    t          |                     nh# t          $ r[}t          5  t
                              d           ddd           n# 1 swxY w Y   t                     dd| dcY d}~S d}~ww xY w|t                       t                              d|pd| pd dd!                    d"d#S )%a  Spawn ``runner`` on the daemon executor and return a handle immediately.

    Parameters
    ----------
    goal, context, toolsets, role, model
        The dispatch-time task spec, captured verbatim for the rich
        completion block.
    session_key
        The gateway session_key (from ``tools.approval.get_current_session_key``)
        captured on the parent thread BEFORE dispatch, because the daemon
        worker thread won't carry the contextvar. Used to route the
        completion back to the originating session.
    parent_session_id
        The durable ``state.db`` session id of the parent agent that spawned
        the delegation. Carried on the completion event so the gateway can
        pin routing to the spawning session instead of recovering the latest
        ``ended_at IS NULL`` row for the peer tuple (#57498).
    runner
        Zero-arg callable that builds + runs the child and returns the same
        result dict ``_run_single_child`` produces. Runs on the worker thread.
    interrupt_fn
        Optional callable to signal the child to stop (used on shutdown /
        explicit cancel).
    progress_fn
        Optional zero-arg callable returning ``(token, in_tool)`` where
        ``token`` is any comparable snapshot of the child's progress (api
        call count + current tool) and ``in_tool`` says whether the child is
        currently inside a tool call. Sampled by the stale monitor; a frozen
        token past the stale threshold marks the delegation stuck (see the
        stale-detection block at the top of this module). When omitted, the
        delegation is not monitored.
    max_async_children
        Concurrency cap. When at capacity the dispatch is REJECTED (the caller
        should fall back to sync or tell the user) rather than queued, so a
        runaway model can't pile up unbounded background work.

    Returns
    -------
    dict
        ``{"status": "dispatched", "delegation_id": ...}`` on success, or
        ``{"status": "rejected", "error": ...}`` when at capacity.
    r[   rT   rV   rW   NrX   rY   r\   r^   r@   r_   r~   r   r`   r   r  r  _progress_token_progress_ts_interrupted_atc              3  H   K   | ]}|                     d           dv dV  dS r~   r   r   r4   NrP   r   s     r   r   z,dispatch_async_delegation.<locals>.<genexpr>  D       
 
uuX"999 9999
 
r   rejected#Async delegation capacity reached (z running). Wait for one to finish (its result will re-enter the chat), or run this task synchronously (background=false). Raise delegation.max_concurrent_children in config.yaml to allow more concurrent background subagents.r~   r   r   r/   c            	        i } d}	              pi } |                      d          pd}nu# t          $ rh}t                              d           dd t	          |          j         d| dt          t          j                    z
  d          d} d}Y d }~nd }~ww xY wt          | |           d S # t          | |           w xY w)	Nr   r~   r   zAsync delegation %s crashed: r   r   )r~   r   r   	api_callsduration_seconds)	rQ   r*   r   	exceptionr   __name__roundra   	_finalize)r|   r~   excr[   r`   r  s      r   _workerz*dispatch_async_delegation.<locals>._worker  s    !#	5VXX^FZZ))8[FF 		 		 		:MJJJ! II.77#77$)$)++*Eq$I$I F FFFFFF		 mVV44444ImVV4444s,   #+ B3 
BABB3 BB3 3Cz%Failed to schedule async delegation: z3Dispatched async delegation %s (session_key=%s): %s<cli>r]   P   
dispatchedr~   r[   r   r/   )r   ra   r   r   r   r   r   rk   r   submitr   r*   r  ro   _ensure_stale_monitorr   info)rT   rV   rW   rX   rY   r\   r_   r  r^   r@   r  r  r  rJ   r   executorr(  r'  r[   r`   s          `          @@r   dispatch_async_delegationr2    s   t '((MIKKM 	7 	h8DNNND	
 	 	 	{ 	 4 	. 	. 	) 	 	 	 	{" 	4#$ 	%& 	4' F0 
 ) ) 
 
((
 
 
 
 
 ((($R:L R R R	 	) ) ) ) ) ) ) )  #)!) ) ) ) ) ) ) ) ) ) ) ) ) ) )$ f/00H5 5 5 5 5 5 5 5&
 	3G<<==== 
 
 
 	. 	.LL---	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	."=111 BSBB
 
 	
 	
 	
 	
 	
 	
	
 
KK={-g
CRC/@   #]CCCsZ   6?C
CCC
"D- -
F7F>E&F&E*	*F-E*	.FFFr~   c                x    t          |           }|dS |\  }}t          |||           t          | |           dS )zDMark a record complete and push the completion event onto the queue.N)_begin_finalization_push_completion_event_finish_finalization)r[   r|   r~   claimedevent_record_interrupt_fns         r   r&  r&  7  sK    !-00G")L-<888/////r   =Optional[tuple[Dict[str, Any], Optional[Callable[[], None]]]]c                \   t           5  t                              |           }||                    d          dvr	 ddd           dS d|d<   t          j                    |d<   |                    d          }d|d<   d|d<   t	          |          }ddd           n# 1 swxY w Y   ||fS )zCAtomically claim terminal delivery while keeping the record active.Nr~   r  r   r   r  r  )r   r   rQ   ra   r   )r[   rJ   r  r8  s       r   r4  r4  B  s    
 $ $m,,>VZZ119PPP$ $ $ $ $ $ $ $ (x!%~zz.11!%~ $}F||$ $ $ $ $ $ $ $ $ $ $ $ $ $ $ %%s   5B
A	BB#&B#c                    t           5  t                              |           }|||d<   t                       d d d            d S # 1 swxY w Y   d S )Nr~   )r   r   rQ   r  )r[   r~   rJ   s      r   r6  r6  W  s    	 " "m,,%F8!!!	" " " " " " " " " " " " " " " " " "r   c           	     ^   	 ddl m} nG# t          $ r:}t                              d|                     d          |           Y d}~dS d}~ww xY w|                    d          }|                    d          }|                     d          pt          j                    }|                     d	          pt          j                    }i d
dd|                     d          d|                     dd          d|                     dd          d|                     dd          d|                     d          d|                     dd          d|                     d          d|                     d          d|                     d          d|                    d          p|                     d          d|d|d|d|                    dd          d|                    dt          ||z
  d                    d|||                    d          d}	dD ]}
|
|v r||
         |	|
<   t          |	|           	 |j	        
                    |	           dS # t          $ r:}t                              d|                     d          |           Y d}~dS d}~ww xY w)zPush a type='async_delegation' event onto the shared completion queue.

    Best-effort: a failure here must not crash the worker, but it WOULD mean a
    silently-lost result, so we log loudly.
    r   process_registryzPAsync delegation %s finished but process_registry import failed; result lost: %sr[   Nr   r   r`   r   r   r   r\   r]   r^   r@   r_   rT   rV   rW   rX   rY   r~   r!  r"  r   exit_reason)r   r@  stalled_after_quiet_secondsstall_threshold_secondsstall_phasestall_grace_secondszHAsync delegation %s: failed to enqueue completion event; result lost: %s)tools.process_registryr?  r*   r   r   rQ   ra   r%  r   completion_queuer   )rJ   r|   r~   r?  r'  r   r   r`   r   r   _ks              r   r5  r5  _  sN   ;;;;;;;   JJ''	
 	
 	

 	 jj##GJJwEJJ//>49;;M::n--<L"O44
 	vzz-44 	

+A2 F F 	VZZ(;R@@ 	VZZ(;<< 	

62&& 	6::i(( 	FJJz** 	

6"" 	G$$;

7(;(; 	& 	7  	!" 	VZZQ//#$ 	FJJl]&BA F F
 
%* 	+, %zz-00/  C6 ! ! <<RjCGV$$$
)--c22222 
 
 
JJ''	
 	
 	
 	
 	
 	
 	
 	
 	

s,   	 
A/AAI( (
J,2/J''J,)r_   r^   r@   r  r  r[   r  rU   	List[str]c                8   pt                      t          j                    t          |           }|dk    r| d         n$| dd                    d | D                       z   }i dd|dt	          |           d	|d
|rt	          |          ndd|d|d|d|d|	d|dddddd|
ddd|ddd}t
          5  t          d t                                          D                       }||k    rdd| ddcddd           S |t          <   ddd           n# 1 swxY w Y   t          |           t          |          }d(fd"}	 |                    t          |                     nh# t          $ r[}t
          5  t                              d           ddd           n# 1 swxY w Y   t                     dd#| dcY d}~S d}~ww xY w|t!                       t"                              d$||pd%           d&d'S ))u+  Dispatch a WHOLE fan-out batch as ONE background unit.

    Unlike ``dispatch_async_delegation`` (which backs a single subagent),
    ``runner`` here runs the entire batch — it builds and joins on every child
    in parallel and returns the combined ``{"results": [...],
    "total_duration_seconds": N}`` dict that the synchronous path would have
    returned. We occupy ONE async slot for the whole batch (the in-batch
    parallelism is bounded separately by ``max_concurrent_children``), so a
    single ``delegate_task`` fan-out never exhausts the async pool by itself.

    When the batch finishes, a SINGLE completion event is pushed onto the
    shared ``process_registry.completion_queue`` carrying the full per-task
    ``results`` list, so the consolidated summaries re-enter the conversation
    as one message once every child is done — the chat is never blocked while
    they run.

    Returns ``{"status": "dispatched", "delegation_id": ...}`` on success or
    ``{"status": "rejected", "error": ...}`` when the async pool is at
    capacity.
    r4   r   z parallel subagents: z; c              3  *   K   | ]}|d d         V  d S )N(   r   )r5   gs     r   r   z2dispatch_async_delegation_batch.<locals>.<genexpr>  s+      G^G^ST#2#G^G^G^G^G^G^r   r[   rT   rU   rV   rW   NrX   rY   r\   r^   r@   r_   r~   r   r`   r   r  rZ   Tr  )r  r  r  c              3  H   K   | ]}|                     d           dv dV  dS r  rP   r   s     r   r   z2dispatch_async_delegation_batch.<locals>.<genexpr>  r  r   r  r  z running). Wait for one to finish (its result will re-enter the chat), or raise delegation.max_concurrent_children in config.yaml to allow more concurrent background units.r  r   r/   c                    i } d}	              pi } |                      d          pg }|rt          d |D                       rd}nd}ns# t          $ rf}t                              d           g t          |          j         d| t          t          j                    z
  d          d} d}Y d }~nd }~ww xY wt          | |           d S # t          | |           w xY w)	Nr   resultsc              3  D   K   | ]}|                     d           dvV  dS )r~   )r   successNrP   r   s     r   r   zCdispatch_async_delegation_batch.<locals>._worker.<locals>.<genexpr>  sF       % % x(@@% % % % % %r   r   z!Async delegation batch %s crashedr   r   rP  r   total_duration_seconds)
rQ   allr*   r   r#  r   r$  r%  ra   _finalize_batch)combinedr~   child_resultsr'  r[   r`   r  s       r   r(  z0dispatch_async_delegation_batch.<locals>._worker  s5   #%	=vxx~2H$LL339rM % % %&% % % " " % !$ 	 	 	@-PPP II.77#77*/	m0KQ*O*O H
 FFFFFF	 M8V<<<<<OM8V<<<<s1   AA 
C 
B;AB61C 6B;;C C$z+Failed to schedule async delegation batch: zADispatched async delegation batch %s (%d task(s), session_key=%s)r)  r+  r,  r-  )r   ra   r   joinr   r   r   r   r   rk   r   r.  r   r*   r  ro   r/  r   r0  )rU   rV   rW   rX   rY   r\   r_   r  r^   r@   r  r  r[   r  ncombined_goalrJ   r   r1  r(  r'  r`   s          `    `        @r   dispatch_async_delegation_batchr\    s   J "9%7%9%9MIKKME

A FFa1 ; ; ;diiG^G^X]G^G^G^>^>^ ^  	e 	7	
 	h8DNNND 	 	 	{ 	 4 	. 	. 	) 	 	 	  	D!" 	{#$  %)  F, 
 ) ) 
 
((
 
 
 
 
 ((($N:L N N N ) ) ) ) ) ) ) ) #)) ) ) ) ) ) ) ) ) ) ) ) ) ) )" f/00H= = = = = = = =2

3G<<==== 
 
 
 	. 	.LL---	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	."=111 H3HH
 
 	
 	
 	
 	
 	
 	
	
 
KKKq+0   #]CCCsZ   	?D+
D++D/2D/"F   
G%
G F9-G 9F=	=G  F=	G G% G%rW  c                x    t          |           }|dS |\  }}t          |||           t          | |           dS )zDMark a batch record complete and push ONE combined completion event.N)r4  _push_batch_completion_eventr6  )r[   rW  r~   r7  r8  r9  s         r   rV  rV  (  sM     "-00G")L- x@@@/////r   r8  c                   	 ddl m} nG# t          $ r:}t                              d|                     d          |           Y d}~dS d}~ww xY w|                     d          pt          j                    }|                     d          pt          j                    }i dd	d|                     d          d
|                     d
d          d|                     dd          d|                     dd          d|                     d          d|                     dd          d|                     d          d|                     d          d|                     d          d|                     d          d|                     d          d|ddd|                    d          pg d|                    d          d|                    d          |                    d          ||d}dD ]}||v r||         ||<   t          ||           	 |j        	                    |           dS # t          $ r:}t                              d|                     d          |           Y d}~dS d}~ww xY w)z8Push a combined async-delegation batch completion event.r   r>  zVAsync delegation batch %s finished but process_registry import failed; result lost: %sr[   Nr`   r   r   r   r\   r]   r^   r@   r_   rT   rU   rV   rW   rX   rY   r~   rZ   TrP  live_transcriptsr   rT  )rT  r`   r   rA  zNAsync delegation batch %s: failed to enqueue completion event; result lost: %s)
rF  r?  r*   r   r   rQ   ra   r   rG  r   )	r8  rW  r~   r?  r'  r`   r   r   rH  s	            r   r^  r^  5  sC   ;;;;;;;   &_--s	
 	
 	

 	 !$$_55DM##N33Bty{{L"))/:: 	|''r:: 	 0 01G L L	
 	\--.A2FF 	\--.ABB 	  ,, 	!!'** 	<##I.. 	L$$Z00 	  (( 	!!'** 	& 	D" 	8<<	**0b#* 	HLL);<<+, 	g&&-. #+,,/G"H"H&$3  C: # # >>rlCGX&&&
)--c22222 
 
 
_--s	
 	
 	
 	
 	
 	
 	
 	
 	

s,   	 
A/AA#H? ?
J	/I>>Jc                 >   t           5  t          't                                          r	 ddd           dS t                                           t          j        t          dd          at                                           ddd           dS # 1 swxY w Y   dS )zStart (once) the module-level stale-delegation monitor thread.

    One daemon thread serves every dispatch; it exits on its own when no
    monitorable records remain, and is restarted by the next dispatch that
    carries a ``progress_fn``.
    Nzasync-delegate-stale-monitorT)targetrE   daemon)	_monitor_lockr   is_alive_monitor_stopclear	threadingThread_stale_monitor_loopstartr   r   r   r/  r/  u  s     
 	  	 &?+C+C+E+E&	  	  	  	  	  	  	  	  	#*&/
 
 

 		  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	 s   "BABBBc            
        t                               t                    st          j                    } g }g }d}t          5  t
                                          D ]}|                    d          }|dk    rCd}|                    d          p| }| |z
  t          k    r|	                    |d                    a|dk    rh|                    d          }|d}	  |            \  }}	n'# t          $ r |                    d
          d}	}Y nw xY w||                    d
          k    r||d
<   | |d<   | |                    d          p| z
  }
|	rt          nt          }|
|k    rsd|d<   | |d<   t          |
d          |d<   ||d<   t          |	          |d<   |	                    |d         t          |                    d                    |
|	f           	 d	d	d	           n# 1 swxY w Y   |D ]\  }}}
}	t                              d||
|	t                     t          5  t
                              |          }|r|                    d          nd	}d	d	d	           n# 1 swxY w Y   t#          |          r?	  |             # t          $ r&}t                              d||           Y d	}~d	}~ww xY w|D ]}t'          |           |sd	S t                               t                    d	S d	S )uY  Sweep running delegations for stalled progress.

    Per sweep, for every running record with a ``progress_fn``:

    - Sample ``(token, in_tool)``. A changed token refreshes the record's
      progress timestamp — a child that keeps advancing is never touched, no
      matter how long it runs.
    - A frozen token past the idle/in-tool threshold marks the record
      ``stalling``: we call ``interrupt_fn`` so a responsive-but-slow child
      can unwind and deliver its (partial) result through the normal
      ``_finalize`` path with full fidelity.
    - A ``stalling`` record whose runner still hasn't returned after the
      grace window is force-finalized with one terminal ``stalled`` event so
      the owning session hears an outcome and the async slot frees. A late
      runner return after that is ignored by ``_begin_finalization``.
    Fr~   r   Tr  r[   r   r  Nr  r  r   _stall_quiet_seconds_stall_threshold_seconds_stall_in_toolrZ   u`   Async delegation %s made no progress for %.0fs (in_tool=%s) — interrupting; grace window %.0fsr  z.Async delegation %s stall interrupt failed: %s)rf  wait_STALE_CHECK_INTERVALra   r   r   r   rQ   _STALL_GRACE_SECONDSappendr*   _STALE_IN_TOOL_SECONDS_STALE_IDLE_SECONDSr%  r   r   r   callabledebug_finalize_stalled)ri   stalledexpiredany_monitorablerJ   r~   interrupted_atr  tokenin_tool	quiet_forlimitr[   	_is_batchfnr'  s                   r   rj  rj    sD   "   !677 Hikk! .	 .	"//++ - -H--Z''&*O%+ZZ0A%B%B%IcN^+/CCCvo'>???Y&&$jj77&"&J%0[]]NE77  J J J &,ZZ0A%B%BE7EEEJ FJJ'8999905F,--0F>*6::n#=#=#DE	.5N**;N  %%'1F8$03F,-
 6;9a5H5HF129>F56/3G}}F+,NN"?3 J!7!788%#	  M-.	 .	 .	 .	 .	 .	 .	 .	 .	 .	 .	 .	 .	 .	 .	^ =D 	 	8M9iNNDy'3G  
  D D!m4439CVZZ///tD D D D D D D D D D D D D D D || BDDDD    LLH%s        % 	- 	-Mm,,,, 	FQ   !677 H H H H Hsb   BG)C,+G),!DG)DCG))G-0G-'4I''I+	.I+	
J
J=J88J=c           
     n   t          |           }|dS |\  }}|                    d          pt          j                    }t          ||                    d          p|z
  d          }|                    d          }|                    d          }|                    d          }d|  d	}	t                              d
| |           |||rdn|dndt          d}
|                    d          rt          |g |	|d|
d           nt          |dd|	d|dd|
d           t          | d           dS )zAForce-finalize a stalling delegation whose runner never returned.Nr   r`   r   rm  rn  ro  zAsync delegation u^   stalled: the detached subagent stopped making progress (no new API calls, tool activity, or streamed tokens), did not respond to interruption, and never produced a completion event. The worker may be wedged inside a model API call — this is a known failure mode of long-lived gateway processes (#60203). Re-dispatch the task if it is still needed.z:Async delegation %s force-finalized as stalled after %.0fsr~  idlerA  rZ   rS  ry  r   )r~   r   r   r!  r"  r@  )
r4  rQ   ra   r%  r   r   rr  r^  r5  r6  )r[   r7  r8  r9  r   durationquiet_secondsthreshold_secondsstall_in_toolr   
stall_metas              r   rx  rx    s   !-00G")L-##N33Bty{{L((99I\J	 H !$$%;<<M$(()CDD $$%566M	M 	 	 	 
 LLDx   (5#4& II(43	 	J 
## 
$*2  	 		
 		
 		
 		
 	#$,(   	
 	
 	
 	22222r   r}  r   ri   floatOptional[List]c                   	 t          |           }n# t          $ r Y dS w xY wg }|D ]}t          |t           t          f          rt	          |          dk    r|d         |d         d}t	          |          dk    rYt          |d         t
          t          f          r7t          t          d|t          |d                   z
            d          |d<   |	                    |           |	                    d           |S )	ug  Parse a progress token into per-child activity dicts (best-effort).

    delegate_tool's ``_batch_progress`` emits one ``(api_call_count,
    current_tool, last_activity_ts)`` tuple per child. Foreign token shapes
    (custom dispatchers) degrade to ``None`` entries rather than raising —
    the token contract is intentionally opaque to the registry.
    Nr   r   r4   )r!  current_toolr   g        seconds_since_activity)
r   	TypeErrorr   r   r   r   r  r%  rs   rs  )r}  ri   partsoutpartentrys         r   _children_activity_from_tokenr  +  s   U   tt*,C  dT5M** 	s4yyA~~!!W $Q% %E 4yyA~~*T!WsEl"C"C~27S5a>>122A3 3./ JJuJJtJs    
  List[Dict[str, Any]]c                 ^   t          j                     } i }t          5  g }t                                          D ]}d |                                D             }|                    d          }|dv r\|                    d          }|rt          | |z
  d          |d<   |                    d          }t          |          r|||d         <   |d	v r5d
D ]2\  }}	|                    |          |                    |          ||	<   3|                    |           	 ddd           n# 1 swxY w Y   |D ]u}|                    |                    d                    }|-	  |            \  }
}n# t          $ r Y Hw xY wt          |
|           }|||d<   t          |          |d<   v|S )a  Snapshot of async delegations (running + recently completed).

    Safe to call from any thread. Excludes the non-serialisable callables
    and private monitor bookkeeping, but exposes computed live-status
    fields for UIs (#51690):

    - ``seconds_since_progress``: how long the stale monitor has seen a
      frozen progress token (running/stalling records).
    - ``children_activity``: per-child ``{api_calls, current_tool,
      seconds_since_activity}`` sampled live from the dispatch's
      ``progress_fn``.
    - ``stalled_after_quiet_seconds`` / ``stall_threshold_seconds`` /
      ``stall_in_tool``: stall context once the monitor has tripped.
    c                L    i | ]!\  }}|d v	|                     d          ||"S )>   r  r  r  )
startswith)r5   kvs      r   rS   z*list_async_delegations.<locals>.<dictcomp>\  sH       Aq;;;S)) < 1;;;r   r~   r  r  r4   seconds_since_progressr  r[   )r   ry  ))rm  rB  )rn  rC  )ro  r  Nchildren_activityr~  )ra   r   r   r   r   rQ   r%  rv  rs  r*   r  r   )ri   samplersr   r   itemr~   tsr  srcdstr}  r~  activitys                r   list_async_delegationsr  H  s4    )++C$&H	  "" 	 	A GGII  D UU8__F000UU>** H5:38Q5G5GD12UU=))B<< 635HQ/0000! / /HC
 uuSzz-$%EE#JJS	LL/	              <  ( (\\$((?3344:	RTTNE77 	 	 	H	0<<(0D$%w--YLs$   D D**D.1D.%E33
F ?F shutdownreasonc                   d}t           5  d t                                          D             }ddd           n# 1 swxY w Y   |D ]}}|                    d          }t	          |          rW	  |             |dz  }7# t
          $ r9}t                              d|                    d          |           Y d}~ud}~ww xY w~|rt                              d||            |S )	a*  Signal every running async delegation to stop. Returns how many.

    Used on ``/stop`` and gateway shutdown so a dangling background subagent
    can't keep burning tokens with no one listening. The child still emits a
    completion event (status='interrupted') via the normal finalize path.
    r   c                @    g | ]}|                     d           dv |S )r~   r  rP   r   s     r   r   z!interrupt_all.<locals>.<listcomp>  s8     
 
 
uuX"999 999r   Nr  r4   z&interrupt_all: %s interrupt failed: %sr[   z'Interrupted %d async delegation(s) (%s)	r   r   r   rQ   rv  r*   r   rw  r0  )r  counttargetsr   r  r'  s         r   interrupt_allr    se    E	 
 

 
((
 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 

  
 
UU>""B<< 	
   <EE/**C       		  N=ufMMMLs%   $:>>.A>>
C/B<<Csession_endc                     sssdS d}t           5   fdt                                          D             }ddd           n# 1 swxY w Y   |D ]}}|                    d          }t	          |          rW	  |             |dz  }7# t
          $ r9}t                              d|                    d          |           Y d}~ud}~ww xY w~|rt                              d||           |S )	u{  Signal running async delegations owned by ONE session to stop.

    A delegation's lifecycle is bound to the session that spawned it: when
    that session ends, its in-flight background subagents must end with it —
    a completed orphan would otherwise sit on the shared completion queue
    with no live owner, either leaking into another chat or burning tokens
    with no one listening (#55578).

    Selectors (any matching field claims the record):
    - ``origin_ui_session_id``: the live TUI tab/window that commissioned it.
    - ``session_key``: the durable routing key captured at dispatch.
    - ``parent_session_id``: the spawning agent's durable session-db id —
      the right selector for gateway chats, whose ``session_key`` (the
      platform conversation key) SURVIVES a ``/new`` reset while the
      session id rotates.

    Returns how many were interrupted.
    r   c                h    g | ].}|                     d           dv t          |          ,|/S )r~   r  r   r   r   s     r   r   z)interrupt_for_session.<locals>.<listcomp>  s\     	
 	
 	
uuX"999*'%9"3	   : 999r   Nr  r4   z.interrupt_for_session: %s interrupt failed: %sr[   z:Interrupted %d async delegation(s) for ending session (%s)r  )	r\   r^   r_   r  r  r  r   r  r'  s	   ```      r   interrupt_for_sessionr    s   0  3 <M qE	 

 

	
 	
 	
 	
 	
 	
((	
 	
 	


 

 

 

 

 

 

 

 

 

 

 

 

 

 

  
 
UU>""B<< 	
   DEE/**C       		  
H6	
 	
 	
 Ls)   (A		AA=B
C/CCc                    t           5  t          t                              d           dadaddd           n# 1 swxY w Y   t                                           t          5  t          } daddd           n# 1 swxY w Y   | *|                                 r| 	                    d           t          5  t                                           ddd           dS # 1 swxY w Y   dS )z@Test-only: clear all state and tear down the executor + monitor.NF)rp  r   r   r#   )r   r   r  r   rf  setrd  r   re  rY  r   r   rg  )threads    r   _reset_for_testsr    s    
 " " E***	 !	" " " " " " " " " " " " " " "
 	                  foo//A	                   s1   ';??&
A<<B B :C!!C%(C%)r   r   )r-   r   r   r/   )r   rG   )rJ   rK   r   r/   )r[   rl   r   r/   r-  )r{   rK   r|   rK   r   r/   )r   r   )r[   rl   r   r   )r[   rl   r   rl   r   r   )r   rK   r   rl   r   r   )r   rK   r   rl   r   r/   )r[   rl   r   r   )r   r   r   r   )r^   rl   r   r   )
rJ   rK   r\   rl   r^   rl   r_   rl   r   r   )r]   r]   r]   )r\   rl   r^   rl   r_   rl   r   r   )r   rl   )rT   rl   rV   r   rW   r  rX   rl   rY   r   r\   rl   r_   r   r  r  r^   rl   r@   rl   r  r  r  r   r  r  r   rK   )r[   rl   r|   rK   r~   rl   r   r/   )r[   rl   r   r:  )r[   rl   r~   rl   r   r/   )rJ   rK   r|   rK   r~   rl   r   r/   )rU   rI  rV   r   rW   r  rX   rl   rY   r   r\   rl   r_   r   r  r  r^   rl   r@   rl   r  r  r  r   r[   r   r  r  r   rK   )r[   rl   rW  rK   r~   rl   r   r/   )r8  rK   rW  rK   r~   rl   r   r/   )r}  r   ri   r  r   r  )r   r  )r  )r  rl   r   r   )r]   r]   r]   r  )
r\   rl   r^   rl   r_   rl   r  rl   r   r   )`__doc__
__future__r   rf   loggingr'   rh  ra   r   concurrent.futuresr   
contextlibr   typingr   r   r   r	   r
   r   hermes_constantsr   tools.daemon_poolr   tools.thread_contextr   	getLoggerr$  r   r   r   __annotations__Lockr   r   r   r   _DEFAULT_MAX_ASYNC_CHILDRENrt   rq   ru   r   re   rq  ru  rt  rr  rd  r   Eventrf  r   r.   r)   rI   rk   ro   rh   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r  r2  r&  r4  r6  r5  r\  rV  r^  r/  rj  rx  r  r  r  r  r  r   r   r   <module>r     s*  ! ! !F # " " " " "          1 1 1 1 1 1 % % % % % % @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ , , , , , , 6 6 6 6 6 6 < < < < < <		8	$	$
 5  +/	 . . . .!!     	   ') ( ( ( (  -  
  9>.     	  .2 2 2 2 2	!!* * *   )X )X )X )XX    $   :^ ^ ^ ^
$ $ $ $N	
 	
 	
 	

 
 
 
0 0 0 0f   6	! 	! 	! 	!! ! ! !(T T T T!! !! !! !!H! ! ! !,! ! ! !T T T T
S S S S
   &   &
 
 
 
 
 
 
 
   4  "       "
 
 
 
 
2+ + + +       $   H (, "15915OD OD OD OD OD ODd0 0 0 0& & & &*" " " "B
 B
 B
 B
Z (, "159#'15AD AD AD AD AD ADH
0 
0 
0 
0=
 =
 =
 =
@       (Y Y Y YxC3 C3 C3 C3L   :; ; ; ;|    <  "	6 6 6 6 6r     r   