
    sj                   |   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)dZ* ej                     Z+dZ,dZ-dZ.dZ/ ej                     Z0da1ded<    ej2                    Z3d Z4ddZ5dd"Z6edd$            Z7dd&Z8dd(Z9dd+Z:dd,Z;dd/Z<dd0Z=dd1Z>dd2Z?dd4Z@dd6ZAdd:ZBdd;ZCdd<ZDdd=ZEdd>ZFdd?ZGddAZHddDZIddEZJddGZKddHZLdIdIdIdJddMZM	 	 	 dddNZNddOZOddPZPddQZQddIdIde%ddRddaZRddcZSddeZTddfZUddgZVddIdIde%dddhddkZWddmZXddoZYddpZZddqZ[ddrZ\ddxZ]ddzZ^ddd}Z_	 	 	 	 dddZ`ddZad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     Ag      >@g      |@g     @g      ^@zOptional[threading.Thread]_monitor_threadc                 $    t                      dz  S )Nzstate.dbr        :/home/agent/.hermes/hermes-agent/tools/async_delegation.py_db_pathr   |   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Dict[str, Any]c                 p    i } 	 ddl m} dD ]\  }} ||d          }|r|| |<   n# t          $ r Y nw xY w| S )u&  Snapshot the dispatching turn's routing origin for the completion event.

    Captured on the PARENT thread at dispatch time (the daemon worker doesn't
    carry the contextvars) and persisted with the durable record, so a
    completion replayed after a restart can reconstruct a full SessionSource
    even when the session-store origin and in-memory source cache are gone.
    scope_id matters most: on a relay-fronted deployment the connector's
    fail-closed egress guard needs the tenant discriminator (or a user
    binding) to route a scoped reply; without it, post-restart scoped
    completions bounce with "target not routed to an onboarded tenant"
    (staging 2026-08-09 defect #4). Best-effort — empty values are simply
    omitted so CLI/contextvar-unaware paths persist nothing new.
    r   get_session_env))scope_idHERMES_SESSION_SCOPE_ID)user_idHERMES_SESSION_USER_ID)	user_nameHERMES_SESSION_USER_NAME gateway.session_contextrM   r*   )originrM   evt_keyenv_namevalues        r   _capture_routing_originr[      s      F;;;;;;"
 	( 	(GX
 $OHb11E ("'w	(    Ms   !& 
33recordc                    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   keyr\   s     r   
<dictcomp>z%_persist_dispatch.<locals>.<dictcomp>   s:     
 
 
 &== 	VZZ__ ==r   )
goalgoalscontexttoolsetsrolemodelis_batchrN   rP   rR   a~  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_keyrT   origin_ui_session_idparent_session_iddispatched_atr@   )timegateway.statusr^   
__import__getpidr*   _DB_LOCKrI   rC   rb   jsondumps_prune_durable_records)r\   nowr^   r:   task_payloadr-   s   `     r   _persist_dispatchr{      s   
)++C 99999911*T2B2B2I2I2K2KLL       
 
 
 


 
 
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rl   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=?)ru   rI   rC   rl   r-   s     r   _delete_durable_delegationr     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)	rq   _DURABLE_RETENTION_SECONDSru   rI   rC   fetchonemax_MAX_RETAINED_COMPLETED_MAX_DURABLE_PENDING)ry   cutoffr-   terminal_countexcesspending_countoverflows          r   rx   rx     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_atrl   )rq   ru   rI   rC   rb   rv   rw   )r   r   ry   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=?)ru   rI   rC   rq   r~   s     r   _note_delivery_attemptr   G  s    	 
 
<>> 
TtY[[-(	
 	
 	

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
s4   A%*AA%A	A%A	A%%A),A)c                    	 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 ]"}|                    |          r||         ||<   #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_existsr^   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_delegationrl   rm   rn   r@   rT   ro   re   rf   rg   rh   ri   rj   rk   r   unknownsummaryerrorzLDelegation owner exited before recording a terminal result; outcome unknown.rp   r   rN   rP   rR   )r   r   r   zUPDATE async_delegations SET state='unknown', completed_at=?,
                   updated_at=?, event_json=?, result_json=?, delivery_state='pending'
                   WHERE delegation_id=?r4   )rr   r   r^   r*   rq   ru   rI   rC   fetchallr   rv   loadsrb   boolrw   )r   r^   ry   	recoveredr-   rowsr6   rl   rm   	origin_ui	parent_idrp   pidstartedr;   r@   livetaskr   _kr   s                        r   recover_abandoned_delegationsr   O  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" ; ) )88B<< ) $RE"I )dU7^TTFLL, c4:e,,dj.@.@-P	   NIIM&	- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\ sB    
IG H4(I4H8	8I;H8	<IIIc           
     z   t                       t          j                    }d}t          5  t                      5 }|                    d                                          }|D ]\  }}}}|p|}	|	rR||	z
  t          k    rD|                    d||f           t                              d|||	z
  dz  t          dz             _t          j
        |          }
t          |
t                    rd|
d<   |                     |
           |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 )
u  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).

    Staleness cap: a pending completion older than
    ``_MAX_COMPLETION_REPLAY_AGE_S`` is terminally dropped instead of
    replayed. Replaying a weeks-old completion re-runs its parent session as
    a full-context turn (a July session replayed in August burned a
    102K-token context on the staging fleet) for a result nobody is waiting
    on anymore; the payload stays queryable on the dropped row.
    r   zSELECT delegation_id, event_json, completed_at, dispatched_at
               FROM async_delegations
               WHERE state != 'running' AND delivery_state='pending' AND event_json IS NOT NULL
               ORDER BY completed_at, delegation_idzUPDATE async_delegations SET delivery_state='dropped',
                              delivery_claim=NULL, delivery_claimed_at=NULL,
                              updated_at=?
                       WHERE delegation_id=? AND delivery_state='pending'z|Async delegation %s: pending completion is %.1fh old (cap %.1fh); terminally dropping the replay (result remains queryable).g      @Trestoredr4   N)r   rq   ru   rI   rC   r   _MAX_COMPLETION_REPLAY_AGE_Sloggerwarningrv   r   
isinstancedictput)target_queuery   r   r-   r   rl   payloadr   rp   	age_basisevts              r   restore_undelivered_completionsr     s   & "###
)++CH	  <>> T||7
 

 (** 	 DH 	 	?M7L-$5I cIo1MMMM -(   * "C)Ov#=069   *W%%C#t$$ '"&JS!!!MHH-	                             < Os5   D0CDD0D	D0 D	!D00D47D4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rq   ru   rI   rC   rowcount)rl   ry   r-   curs       r   mark_completion_deliveredr     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   )rq   ru   rI   rC   r   r   )rl   r   ry   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   rT   rl   :r_   N)rb   r|   rs   rt   uuiduuid4hexr   )r   r   rl   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=?)rq   ru   rI   rC   _MAX_DELIVERY_ATTEMPTSr   r   r   )rl   r   ry   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   rl   r   ry   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   rl   rT   )rb   r   r|   r   r   s     r   complete_event_deliveryr   3  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   )rb   r   r|   r   s     r   release_event_deliveryr   8  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               rT   )	rl   origin_sessionstaterp   r   r   delivery_statedelivery_attemptsr@   )ru   rI   rC   r   rv   r   )rl   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   P  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   Nra   r5   rs     r   	<genexpr>zactive_count.<locals>.<genexpr>m  sD       
 
uuX"GGG GGGG
 
r   N_records_locksumr   valuesr   r   r   active_countr   c  s     
 
 
 
 
((
 
 
 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
   0AA	A	rn   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   rn   rT   r4   N)rb   r|   )r5   r   rn   s     r   r   z%active_for_session.<locals>.<genexpr>x  sw       
 
uuX"GGGAEE0117R88#$ $ $ $ $ $	
 
r   Nr   )rn   s   `r   active_for_sessionr   s  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   rk   rf   r4   N)r   r   r   rb   r   listtuplelen)totalr   rf   s      r   active_task_countr     s    
 
 
"" 	 	AuuX&???uuZ   gz%$'G'GXEXUWXX

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
s   BB//B36B3rT   rm   rn   ro   rm   ro   c                   |r(t          |                     d          pd          |k    pS|o't          |                     d          pd          |k    p)|o't          |                     d          pd          |k    S )Nrn   rT   rm   ro   )r|   rb   )r\   rm   rn   ro   s       r   _matches_session_selectorsr    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rb   r  r5   r   rn   ro   rm   s     r   r   z'has_live_for_session.<locals>.<genexpr>  sl       	
 	
  EE(OODD *'%9"3	  	
 	
 	
 	
 	
 	
r   N)r   anyr   r   r   s   ```r   has_live_for_sessionr    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"    *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   ra   )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   rp   r   ra   )kvs    r   <lambda>z)_prune_completed_locked.<locals>.<lambda>  s1    "Q%))N";";"^r!uyy?Y?Y"^]^ r   )rc   )r   itemsr   r   sortpop)r   r  _s      r   _prune_completed_lockedr    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   rL   HERMES_SESSION_PLATFORMrT   
api_serverHERMES_SESSION_CHAT_IDrU   rL   s    r   _current_origin_session_idr    sp    $;;;;;;?4b99\II27<<BB   rrs   ( ( 
66)ro   rn   r@   interrupt_fnmax_async_childrenprogress_fnre   rg   rh   Optional[List[str]]ri   rj   runnerCallable[[], Dict[str, Any]]r@   r  Optional[Callable[[], None]]r  r  Optional[Callable[[], tuple]]c                   t                      t          j                    | ||rt          |          nd|||||	|d
t                      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| 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.
    N)
rl   re   rg   rh   ri   rj   rm   rn   r@   ro   r   )r   rp   r   r  r  _progress_token_progress_ts_interrupted_atc              3  H   K   | ]}|                     d           dv dV  dS r   r   r   r4   Nra   r   s     r   r   z,dispatch_async_delegation.<locals>.<genexpr>G  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)	rb   r*   r   	exceptionr   __name__roundrq   	_finalize)r   r   excrl   rp   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>rT   P   
dispatchedr   rl   r   r/   )r
  rq   r   r[   r   r   r   r   r{   r   submitr   r*   r  r   _ensure_stale_monitorr   info)re   rg   rh   ri   rj   rm   ro   r   rn   r@   r  r  r  r\   r   executorr8  r7  rl   rp   s          `          @@r   dispatch_async_delegationrB    s   t '((MIKKM&&.8DNNND" 4..  "
#
# &$"%)  F2 
 ) ) 
 
((
 
 
 
 
 ((($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   #?C/
CC	C	7"D 
E?$E:+EE:E	E:E	E:4E?:E?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)rl   r   r   claimedevent_record_interrupt_fns         r   r6  r6    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   rb   rq   r   )rl   r\   r  rH  s       r   rD  rD    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   rb   r  )rl   r   r\   s      r   rF  rF    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 ]"}
|                     |
          r| |
         |	|
<   #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: %srl   Nr   r   rp   r   r   r   rm   rT   rn   r@   ro   re   rg   rh   ri   rj   r   r1  r2  r   exit_reason)r   rP  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_registryrO  r*   r   r   rb   rq   r5  r   completion_queuer   )r\   r   r   rO  r7  r   r   rp   r   r   r   s              r   rE  rE    s   ;;;;;;;   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/  C8 3 ! !::b>> 	!RjCG ! ! <<RjCGV$$$
)--c22222 
 
 
JJ''	
 	
 	
 	
 	
 	
 	
 	
 	

s,   	 
A/AA1J 
K/KK)ro   rn   r@   r  r  rl   r  rf   	List[str]c                   pt                      t          j                    t          |           }|dk    r| d         n$| dd                    d | D                       z   }|t	          |           ||rt	          |          nd|||||	|dt                      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   N)rl   re   rf   rg   rh   ri   rj   rm   rn   r@   ro   r   T)	r   rp   r   r  rk   r  r%  r&  r'  c              3  H   K   | ]}|                     d           dv dV  dS r)  ra   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   successNra   r   s     r   r   zCdispatch_async_delegation_batch.<locals>._worker.<locals>.<genexpr>U  sF       % % x(@@% % % % % %r   r   z!Async delegation batch %s crashedr0  r   r_  r   total_duration_seconds)
rb   allr*   r   r3  r   r4  r5  rq   _finalize_batch)combinedr   child_resultsr7  rl   rp   r   s       r   r8  z0dispatch_async_delegation_batch.<locals>._workerN  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)r9  r;  r<  r=  )r
  rq   r   joinr   r[   r   r   r   r   r{   r   r>  r   r*   r  r   r?  r   r@  )rf   rg   rh   ri   rj   rm   ro   r   rn   r@   r  r  rl   r  ncombined_goalr\   r   rA  r8  r7  rp   s          `    `        @r   dispatch_async_delegation_batchrk    ss   J "9%7%9%9MIKKME

A FFa1 ; ; ;diiG^G^X]G^G^G^>^>^ ^  'e&.8DNNND" 4..  "
#
# &$"%+  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   6?D
DDD
"E- -
G7G>F&G&F*	*G-F*	.GGGrf  c                x    t          |           }|dS |\  }}t          |||           t          | |           dS )zDMark a batch record complete and push ONE combined completion event.N)rD  _push_batch_completion_eventrF  )rl   rf  r   rG  rH  rI  s         r   re  re  |  sM     "-00G")L- x@@@/////r   rH  c                V   	 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 ]"}|                     |          r| |         ||<   #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   rN  zVAsync delegation batch %s finished but process_registry import failed; result lost: %srl   Nrp   r   r   r   rm   rT   rn   r@   ro   re   rf   rg   rh   ri   rj   r   rk   Tr_  live_transcriptsr   rc  )rc  rp   r   r   rQ  zNAsync delegation batch %s: failed to enqueue completion event; result lost: %s)
rV  rO  r*   r   r   rb   rq   r   rW  r   )	rH  rf  r   rO  r7  rp   r   r   r   s	            r   rm  rm    sy   ;;;;;;;   &_--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  C8 3 ' 'B 	'"2&CG # # >>rlCGX&&&
)--c22222 
 
 
_--s	
 	
 	
 	
 	
 	
 	
 	
 	

s,   	 
A/AAI$ $
J(./J##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?    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'  rl   r   r  Nr%  r&  r   _stall_quiet_seconds_stall_threshold_seconds_stall_in_toolrk   u`   Async delegation %s made no progress for %.0fs (in_tool=%s) — interrupting; grace window %.0fsr  z.Async delegation %s stall interrupt failed: %s)ru  wait_STALE_CHECK_INTERVALrq   r   r   r   rb   _STALL_GRACE_SECONDSappendr*   _STALE_IN_TOOL_SECONDS_STALE_IDLE_SECONDSr5  r   r   r   callabledebug_finalize_stalled)ry   stalledexpiredany_monitorabler\   r   interrupted_atr  tokenin_tool	quiet_forlimitrl   	_is_batchfnr7  s                   r   ry  ry    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   rp   r   r|  r}  r~  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  idlerQ  rk   rb  r  r   )r   r   r   r1  r2  rP  )
rD  rb   rq   r5  r   r   r  rm  rE  rF  )rl   rG  rH  rI  r   durationquiet_secondsthreshold_secondsstall_in_toolr   
stall_metas              r   r  r  =  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   ry   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   )r1  current_toolr   g        seconds_since_activity)
r   	TypeErrorr   r   r   r   r  r5  r   r  )r  ry   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   rd   z*list_async_delegations.<locals>.<dictcomp>  sH       Aq;;;S)) < 1;;;r   r   r*  r&  r4   seconds_since_progressr  rl   )r   r  ))r|  rR  )r}  rS  )r~  r  Nchildren_activityr  )rq   r   r   r   r  rb   r5  r  r  r*   r  r   )ry   samplersr  r   itemr   tsr  srcdstr  r  activitys                r   list_async_delegationsr    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*  ra   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: %srl   z'Interrupted %d async delegation(s) (%s)	r   r   r   rb   r  r*   r   r  r@  )r  counttargetsr   r  r7  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: %srl   z:Interrupted %d async delegation(s) for ending session (%s)r  )	rm   rn   ro   r  r  r  r   r  r7  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)r  r   r   r#   )r   r   r  r   ru  setrs  r   rt  rh  r   r   rv  )threads    r   _reset_for_testsr  4  s    
 " " E***	 !	" " " " " " " " " " " " " " "
 	                  foo//A	                   s1   ';??&
A<<B B :C!!C%(C%)r   r   )r-   r   r   r/   )r   rG   )r   rJ   )r\   rJ   r   r/   )rl   r|   r   r/   r=  )r   rJ   r   rJ   r   r/   )r   r   )rl   r|   r   r   )rl   r|   r   r|   r   r   )r   rJ   r   r|   r   r   )r   rJ   r   r|   r   r/   )rl   r|   r   r   )r   r   r   r   )rn   r|   r   r   )
r\   rJ   rm   r|   rn   r|   ro   r|   r   r   )rT   rT   rT   )rm   r|   rn   r|   ro   r|   r   r   )r   r|   )re   r|   rg   r   rh   r  ri   r|   rj   r   rm   r|   ro   r   r   r!  rn   r|   r@   r|   r  r"  r  r   r  r#  r   rJ   )rl   r|   r   rJ   r   r|   r   r/   )rl   r|   r   rJ  )rl   r|   r   r|   r   r/   )r\   rJ   r   rJ   r   r|   r   r/   )rf   rX  rg   r   rh   r  ri   r|   rj   r   rm   r|   ro   r   r   r!  rn   r|   r@   r|   r  r"  r  r   rl   r   r  r#  r   rJ   )rl   r|   rf  rJ   r   r|   r   r/   )rH  rJ   rf  rJ   r   r|   r   r/   )r  r   ry   r  r   r  )r   r  )r  )r  r|   r   r   )rT   rT   rT   r  )
rm   r|   rn   r|   ro   r|   r  r|   r   r   )b__doc__
__future__r   rv   loggingr'   rw  rq   r   concurrent.futuresr   
contextlibr   typingr   r   r   r	   r
   r   hermes_constantsr   tools.daemon_poolr   tools.thread_contextr   	getLoggerr4  r   r   r   __annotations__Lockr   r   r   r   _DEFAULT_MAX_ASYNC_CHILDRENr   r   r   r   r   ru   r  r  r  r  rs  r   Eventru  r   r.   r)   rI   r[   r{   r   rx   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r  r
  r  r  rB  r6  rD  rF  rE  rk  re  rm  r?  ry  r  r  r  r  r  r  r   r   r   <module>r     sL  ! ! !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    $   >       F^ ^ ^ ^
$ $ $ $N	
 	
 	
 	

 
 
 
6 6 6 6r4 4 4 4n	! 	! 	! 	!! ! ! !(T T T T!! !! !! !!H! ! ! !,! ! ! !T T T T
S S S S
   &   &
 
 
 
 
 
 
 
   4  "       "
 
 
 
 
2+ + + +       $   H (, "15915PD PD PD PD PD PDf0 0 0 0& & & &*" " " "H
 H
 H
 H
f (, "159#'15BD BD BD BD BD BDJ
0 
0 
0 
0A
 A
 A
 A
H       (Y Y Y YxC3 C3 C3 C3L   :; ; ; ;|    <  "	6 6 6 6 6r     r   