
    `gjm                        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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% ej:                         Z&d Z'd4dZ(d5dZ)d6dZ*d7dZ+d8dZ,d6dZ-d9dZ.d9dZ/d:dZ0d;dZ1d<dZ2d;dZ3d;d Z4d=d!Z5d=d"Z6d>d#Z7d?d$Z8d9d%Z9d@d&Z:d7d'Z;dd(de"d)	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dAd*Z<dBd+Z=	 	 	 	 	 	 	 	 dCd,Z>dd(de"dd-	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dDd.Z?	 	 	 	 	 	 	 	 dEd/Z@dFd0ZAdGdHd1ZB	 	 	 	 dI	 	 	 	 	 	 	 	 	 dJd2ZCd7d3ZDy)Ku  
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)AnyCallableDict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  c                     t               dz  S )Nzstate.dbr
        I/root/.hermes/venv/lib/python3.12/site-packages/tools/async_delegation.py_db_pathr   S   s    z))r   c                 ^   t               } | j                  j                  dd       t        j                  | d      }|j                  d       |j                  d       |j                  d      D ch c]  }|d   	 }}d	D ]!  \  }}||vs|j                  d
| d|        # |S c c}w )NT)parentsexist_ok
   )timeoutzPRAGMA journal_mode=WALa  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
        )z$PRAGMA table_info(async_delegations)   ))	owner_pidINTEGER)owner_started_atr    )	task_jsonTEXT)delivery_claimr#   )delivery_claimed_atREALz)ALTER TABLE async_delegations ADD COLUMN  )r   parentmkdirsqlite3connectexecute)pathconnrowcolumnsnamesql_types         r   _connectr3   W   s    :DKKdT2??4,DLL*+LL	, "&.T!UV#s1vVGV Xh wLLDTF!H:VWX K Ws   4B*c                z   t        j                          }	 ddlm}  |t        d      j	                               }dD ci c]  }|| v r|| j                  |       }}t        5  t               5 }|j                  d| d   | j                  dd      | j                  d	d      | j                  d
      | d   |t        d      j	                         |t        j                  |      f	       d d d        d d d        t                y # t
        $ r d }Y w xY wc c}w # 1 sw Y   2xY w# 1 sw Y   t                y xY w)Nr   )get_process_start_timeos)goalgoalscontexttoolsetsrolemodelis_batchah  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)
               VALUES (?, ?, ?, ?, 'running', ?, ?, 'pending', 0, ?, ?, ?)delegation_idsession_key origin_ui_session_idparent_session_iddispatched_at)timegateway.statusr5   
__import__getpid	Exceptionget_DB_LOCKr3   r,   jsondumps_prune_durable_records)recordnowr5   r!   keytask_payloadr.   s          r   _persist_dispatchrR      s.   
))+C 91*T2B2I2I2KL
 Y&= 	VZZ_L 
 
 
8: 
N O$fjj&CZZ.3VZZ@S5TO$c:d+;+B+B+Dtzz,79	

 
 )    

 
 
 s;   %D D$D'/A;D*D'DDD$	 D''D:c                    t         5  t               5 }|j                  d| f       d d d        d d d        y # 1 sw Y   xY w# 1 sw Y   y xY w)Nz3DELETE FROM async_delegations WHERE delegation_id=?)rJ   r3   r,   r>   r.   s     r   _delete_durable_delegationrU      sO    	 ^8: ^J]L\]^ ^ ^ ^ ^ ^s   A7AA 	AAc                     t        j                          } | t        z
  }t        5  t               5 }|j	                  d|f       |j	                  d      j                         d   }t        d|t        z
        }|r|j	                  d|f       |j	                  d      j                         d   }t        d|t        z
        }|r|j	                  d|f       ddd       ddd       y# 1 sw Y   xY w# 1 sw Y   yxY w)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)	rD   _DURABLE_RETENTION_SECONDSrJ   r3   r,   fetchonemax_MAX_RETAINED_COMPLETED_MAX_DURABLE_PENDING)rO   cutoffr.   terminal_countexcesspending_countoverflows          r   rM   rM      s   
))+C--F	  8:  _I	
 `

(*Q Q)@@ALL 	 [
 (*Q q-*>>?LL
 3           s#   C4B(C(C4(C1	-C44C=c                b   t        j                          }t        5  t               5 }|j                  d| j	                  dd      | j	                  d|      |t        j                  |       t        j                  |      | d   f       d d d        d d d        y # 1 sw Y   xY w# 1 sw Y   y xY w)NzUPDATE async_delegations SET state=?, completed_at=?, updated_at=?,
               event_json=?, result_json=?, delivery_state='pending'
               WHERE delegation_id=?status	completedcompleted_atr>   )rD   rJ   r3   r,   rI   rK   rL   )eventresultrO   r.   s       r   _persist_completionrg      s    
))+C	 
8: 
( YYx-uyy/MsZZ

6 2E/4JL		

 
 
 
 
 
s#   B%A"BB%B"	B%%B.c                    t         5  t               5 }|j                  dt        j                         | f       d d d        d d d        y # 1 sw Y   xY w# 1 sw Y   y xY w)NzfUPDATE async_delegations SET delivery_attempts=delivery_attempts+1, updated_at=? WHERE delegation_id=?)rJ   r3   r,   rD   rT   s     r   _note_delivery_attemptri      sR    	 
8: 
tYY[-(	

 
 
 
 
 
s!   A'A
A
A	AAc                    	 ddl m} m} t	        j                         }d}t
        5  t               5 }|j                  d      j                         }|D ]S  }|\  }}}	}
}}}}d}|r2 | t        |            }|r| |t        |            t        |      k(  }|rHt        j                  |xs d      }i ddd	|d
|d|	d|
d|j                  dd      d|j                  d      d|j                  d      d|j                  d      d|j                  d      d|j                  d      dt        |j                  d            ddddddd|d|}dd|d   d}|j                  d||t        j                  |      t        j                  |      |f       |dz  }V 	 ddd       ddd       |S # t        $ r Y yw xY w# 1 sw Y   "xY w# 1 sw Y   |S xY w)zEClassify records whose owning process disappeared as outcome unknown.r   )_pid_existsr5   zSELECT delegation_id, origin_session, origin_ui_session_id,
                      parent_session_id, dispatched_at, owner_pid,
                      owner_started_at, task_json
               FROM async_delegations WHERE state IN ('running','finalizing')FNz{}typeasync_delegationr>   r?   rA   rB   r7   r@   r8   r9   r:   r;   r<   r=   rb   unknownsummaryerrorzLDelegation owner exited before recording a terminal result; outcome unknown.rC   rd   )rb   ro   rp   zUPDATE async_delegations SET state='unknown', completed_at=?,
                   updated_at=?, event_json=?, result_json=?, delivery_state='pending'
                   WHERE delegation_id=?r   )rE   rk   r5   rH   rD   rJ   r3   r,   fetchallr   rK   loadsrI   boolrL   )rk   r5   rO   	recoveredr.   rowsr/   r>   r?   	origin_ui	parent_idrC   pidstartedr"   livetaskre   rf   s                      r   recover_abandoned_delegationsr|      sk   F ))+CI	 #8: #||Q

 (* 	  	CgjdM;	9mSRY[dD"3s8,G/1#c(;s7|KD::i/40D
*
,;]
{
,BI
 $Y
 178L
 '*	
 -6txx	7J	

 DHHZ0

 39$((6:J
 '*
 -7TXXj=Q8R
 )
 &/
 g
  
 1?
E !*dU7^TFLL, c4::e,djj.@-P	 NI9	# #H Q  # # #H s4   F= GE9G+G=	G	G	G	GG"c                ~   t                t        5  t               5 }|j                  d      j	                         }|D ]@  \  }}t        j                  |      }t        |t              rd|d<   | j                  |       B 	 ddd       ddd       t              S # 1 sw Y   xY w# 1 sw Y   t              S xY w)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|   rJ   r3   r,   rq   rK   rr   
isinstancedictputlen)target_queuer.   ru   _delegation_idpayloadevts         r   restore_undelivered_completionsr   
  s     "#	 
"8: 
"||7
 (*	 	
 (, 	"#NG**W%C#t$"&JS!		"
" 
" t9
" 
" 
" t9s#   B)A%BB)B&	"B))B<c                    t        j                          }t        5  t               5 }|j                  d||| f      }|j                  dk(  cddd       cddd       S # 1 sw Y   nxY wddd       y# 1 sw Y   yxY w)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'r   NrD   rJ   r3   r,   rowcount)r>   rO   r.   curs       r   mark_completion_deliveredr   %  sq    
))+C	 !8: !llH#}%

 ||q ! ! ! ! ! ! !s"   A2$A
	A2A&	"A22A;c                p   t        j                          }t        5  t               5 }|j                  d| f      j	                         }|	 ddd       ddd       y|j                  d|||| |dz
  f      }|j
                  dk(  cddd       cddd       S # 1 sw Y   nxY wddd       y# 1 sw Y   yxY w)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,  r   )rD   rJ   r3   r,   rX   r   )r>   claim_idrO   r.   r/   r   s         r   claim_completion_deliveryr   1  s    
))+C	 !8: !llP
 (* 	 ;! ! ! llL sCc	:
 ||q ! ! ! ! ! ! !s.   B,%BB,(B	B,B 	B,,B5c                   | j                  d      dk7  ryt        | j                  d      xs d      }|sy| dt        d      j                          dt	        j
                         j                   }t        ||      r|S dS )zCClaim a durable delegation event; non-durable events need no token.rl   rm   r@   r>   :r6   N)rI   strrF   rG   uuiduuid4hexr   )r   consumerr>   r   s       r   claim_event_deliveryr   E  sz    
wwv,,06B7M1Z-4467q9I9I8JKH0I8StSr   c                    t         5  t               5 }|j                  dt        j                         | |f      }|j                  dk(  cddd       cddd       S # 1 sw Y   nxY wddd       y# 1 sw Y   yxY w)z>Release a failed delivery claim so another consumer may retry.zUPDATE async_delegations SET delivery_claim=NULL,
                      delivery_claimed_at=NULL, updated_at=?
               WHERE delegation_id=? AND delivery_state='pending'
                 AND delivery_claim=?r   N)rJ   r3   r,   rD   r   )r>   r   r.   r   s       r   release_completion_deliveryr   P  sm    	 !8: !ll) YY[-2
 ||q ! ! ! ! ! ! !s"   A06A	A0A$	 A00A9c           	         t        j                          }t        5  t               5 }|j                  d||| |f      }|j                  dk(  cddd       cddd       S # 1 sw Y   nxY wddd       y# 1 sw Y   yxY w)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=?r   Nr   )r>   r   rO   r.   r   s        r   complete_completion_deliveryr   ]  st    
))+C	 	!8: 	!ll)
 #}h/
 ||q 	! 	! 	! 	! 	! 	! 	!s"   A3%A	A3A'	#A33A<c                    |r>| j                  d      dk(  r)t        t        | j                  d      xs d      |       y y y Nrl   rm   r>   r@   )rI   r   r   r   r   s     r   complete_event_deliveryr   l  s9    CGGFO'99$S)A)GR%H(S :xr   c                    |r>| j                  d      dk(  r)t        t        | j                  d      xs d      |       y y y r   )rI   r   r   r   s     r   release_event_deliveryr   q  s9    CGGFO'99#C(@(FB$GR :xr   c           	     2   t         5  t               5 }|j                  d| f      j                         }d d d        d d d        y | |d   |d   |d   |d   |d   rt	        j
                  |d         nd |d   |d   d	S # 1 sw Y   NxY w# 1 sw Y   RxY w)
NzSELECT origin_session, state, dispatched_at, completed_at,
                      result_json, delivery_state, delivery_attempts
               FROM async_delegations WHERE delegation_id=?r   r      r            )r>   origin_sessionstaterC   rd   rf   delivery_statedelivery_attempts)rJ   r3   r,   rX   rK   rr   )r>   r.   r/   s      r   get_durable_delegationr   v  s    	 8: ll?AN@P
 (*	 	  {&#a&3q6QQ(+A$**SV$Da&s1v	    s!   B"BBB
	BBc                    t         5  t        	| t        kD  rt        | d      a| at        cddd       S # 1 sw Y   yxY w)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)max_workersthread_name_prefix)_executor_lockr   r   _DaemonThreadPoolExecutor)r   s    r   _get_executorr     sE     
 .C C1'#3I %0!     $5>c                     t         5  t        d t        j                         D              cddd       S # 1 sw Y   yxY w)z.Number of async delegations currently running.c              3  H   K   | ]  }|j                  d       dv sd  yw)rb   >   running
finalizingr   NrI   .0rs     r   	<genexpr>zactive_count.<locals>.<genexpr>  s      `155?F_3_1`s   ""N)_records_locksumr   valuesr   r   r   active_countr     s3    	 a`hoo/``a a ar   c                 J    dt        j                         j                  d d  S )Ndeleg_   )r   r   r   r   r   r   _new_delegation_idr     s#    DJJL$$Ra()**r   c                 :   t         j                         D  cg c]  \  } }|j                  d      dk7  r| |f }} }t        |      t        k  ry|j                  d        |dt        |      t        z
   D ]  \  } }t         j                  | d        yc c}} w )ziDrop the oldest completed records beyond the retention cap.

    Caller must hold ``_records_lock``.
    rb   r   Nc                ^    | d   j                  d      xs | d   j                  d      xs dS )Nr   rd   rC   r   r   )kvs    r   <lambda>z)_prune_completed_locked.<locals>.<lambda>  s+    "Q%))N";"^r!uyy?Y"^]^ r   )rP   )r   itemsrI   r   rZ   sortpop)ridr   rc   _s       r   _prune_completed_lockedr     s     nn&C55?i' 
aI 
 9~00NN^N_Fc)n/FFG  QS$ s   "Br@   )rB   rA   interrupt_fnmax_async_childrenc                   t               t        j                         | ||rt        |      nd|||||dd|	d}t        5  t	        d t
        j                         D              }||
k\  rdd|
 ddcddd       S |t
        <   ddd       t        |       t        |
      }dfd	}	 |j                  t        |             t        j                  d|xs d| xs ddd        ddS # 1 sw Y   jxY w# t        $ rO}t        5  t
        j                  d       ddd       n# 1 sw Y   nxY wt               dd
| dcY d}~S d}~ww xY w)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).
    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.
    Nr   )r>   r7   r9   r:   r;   r<   r?   rA   rB   rb   rC   rd   r   c              3  J   K   | ]  }|j                  d       dk(  sd  ywrb   r   r   Nr   r   s     r   r   z,dispatch_async_delegation.<locals>.<genexpr>  $      
AEE(Oy,HA
   ##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.rb   rp   c            	     h   i } d}	         xs i } | j                  d      xs d}t        | |       y # t        $ ra}t        j                  d       dd t	        |      j
                   d| dt        t        j                         z
  d      d} d}Y d }~sd }~ww xY w# t        | |       w xY w)	Nrp   rb   rc   zAsync delegation %s crashed: r   r   )rb   ro   rp   	api_callsduration_seconds)	rI   rH   logger	exceptionrl   __name__roundrD   	_finalize)rf   rb   excr>   rC   runners      r   _workerz*dispatch_async_delegation.<locals>._worker  s    !#	5X^FZZ)8[F mVV4  		:MJ! I../r#7$)$))+*Eq$IF F		 mVV4s'    5 	BABB" BB" "B1z%Failed to schedule async delegation: z3Dispatched async delegation %s (session_key=%s): %s<cli>r@   P   
dispatchedrb   r>   returnNone)r   rD   listr   r   r   r   rR   r   submitr   rH   r   rU   r   info)r7   r9   r:   r;   r<   r?   rB   r   rA   r   r   rN   r   executorr   r   r>   rC   s          `        @@r   dispatch_async_delegationr     s   ` '(MIIKM&&.DND" 4.&$F$ 
 ) 
(
 
 (($9:L9M NQ R	) ) #))" f/0H5&
 	3G<= KK={-g
CR/@ #]CCq) )V  
 	.LL-	. 	. 	."=1 <SEB
 	
	
sH   2C3	C31C? 3C<?	EED.%	E.D7	3EEEc                p   t         5  t        j                  |       }|
	 ddd       yd|d<   t        j                         |d<   d|d<   t	        |      }ddd       t        ||       t         5  t        j                  |       }|||d<   t                ddd       y# 1 sw Y   LxY w# 1 sw Y   yxY w)zDMark a record complete and push the completion event onto the queue.Nr   rb   rd   r   )r   r   rI   rD   r   _push_completion_eventr   )r>   rf   rb   rN   event_records        r   r   r   5  s    	 
$m,>
$ 
$ (x!%~!%~F|
$ <8	 "m,%F8!	" "
$ 
$" "s   B ,B 0'B, B),B5c           	     V   	 ddl m} |j                  d      }|j                  d      }| j                  d      xs t        j                         }| j                  d	      xs t        j                         }i d
dd| j                  d      d| j                  dd      d| j                  dd      d| j                  d      d| j                  dd      d| j                  d      d| j                  d      d| j                  d      d|j                  d      xs | j                  d      d|d|d|d|j                  dd      d|j                  dt        ||z
  d            d|d	|d|j                  d      i}	t        |	|       	 |j                  j                  |	       y# t        $ r0}t        j	                  d| j                  d      |       Y d}~yd}~ww xY w# t        $ r0}t        j	                  d| j                  d      |       Y d}~y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>   Nro   rp   rC   rd   rl   rm   r?   r@   rA   rB   r7   r9   r:   r;   r<   rb   r   r   r   exit_reasonzHAsync delegation %s: failed to enqueue completion event; result lost: %s)tools.process_registryr   rH   r   rp   rI   rD   r   rg   completion_queuer   )
rN   rf   rb   r   r   ro   rp   rC   rd   r   s
             r   r   r   K  s]   ; jj#GJJwEJJ/>499;M::n-<L"O4
 	vzz-4 	

+A2 F 	VZZ(;< 	

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

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

7(; 	& 	7 	  	VZZQ/!" 	FJJl]&BA F
#( 	)* 	+, 	vzz-0-C0 V$
))--c2O  JJ'	

 	P  
JJ'	
 	

s/   F3 G/ 3	G,<&G''G,/	H(8&H##H()rB   rA   r   r   r>   c                *   xs
 t               t        j                         t        |       }|dk(  r| d   n| ddj                  d | D              z   }|t	        |       ||rt	        |      nd|||||dd|	dd	}t
        5  t        d
 t        j                         D              }||
k\  rdd|
 ddcddd       S |t        <   ddd       t        |       t        |
      }dfd}	 |j                  t        |             t         j#                  d||xs d       ddS # 1 sw Y   cxY w# t        $ rO}t
        5  t        j                  d       ddd       n# 1 sw Y   nxY wt               dd| dcY d}~S d}~ww xY w)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.
    r   r   z parallel subagents: z; c              3  &   K   | ]	  }|d d   y w)N(   r   )r   gs     r   r   z2dispatch_async_delegation_batch.<locals>.<genexpr>  s     G^ST#2G^s   Nr   T)r>   r7   r8   r9   r:   r;   r<   r?   rA   rB   rb   rC   rd   r   r=   c              3  J   K   | ]  }|j                  d       dk(  sd  ywr   r   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   c                    i } d}	         xs i } | j                  d      xs g }|rt        d |D              rd}nd}t        | |       y # t        $ r_}t        j	                  d       g t        |      j                   d| t        t        j                         z
  d      d} d}Y d }~qd }~ww xY w# t        | |       w xY w)	Nrp   resultsc              3  B   K   | ]  }|j                  d       dv  yw)rb   )rc   successNr   r   s     r   r   zCdispatch_async_delegation_batch.<locals>._worker.<locals>.<genexpr>  s&      % x(@@%s   rc   z!Async delegation batch %s crashedr   r   )r   rp   total_duration_seconds)
rI   allrH   r   r   rl   r   r   rD   _finalize_batch)combinedrb   child_resultsr   r>   rC   r   s       r   r   z0dispatch_async_delegation_batch.<locals>._worker  s    #%	=x~2H$LL39rM %&% " !$ M8V<  	@-P I../r#7*/		m0KQ*OH
 F	 M8V<s*   9A 	B6AB1,B9 1B66B9 9Cz+Failed to schedule async delegation batch: zADispatched async delegation batch %s (%d task(s), session_key=%s)r   r   r   r   )r   rD   r   joinr   r   r   r   r   rR   r   r   r   rH   r   rU   r   r   )r8   r9   r:   r;   r<   r?   rB   r   rA   r   r   r>   ncombined_goalrN   r   r   r   r   rC   s          `   `       @r   dispatch_async_delegation_batchr
    s   F "9%7%9MIIKME
A Fa1#%: ;diiG^X]G^>^ ^  'e&.DND" 4.&$F" 
 ) 
(
 
 (($9:L9M NM N) ) #))  f/0H=2

3G<= KKKq+0 #]CCy) )^  
 	.LL-	. 	. 	."=1 B3%H
 	
	
sH   2D.	D.3D: .D7:	FF	E) 	F)E2	.FFFc                   t         5  t        j                  |       }|
	 ddd       yd|d<   t        j                         |d<   d|d<   t	        |      }ddd       	 ddlm} j                  d	      xs t        j                         }|j                  d      xs t        j                         }i d
dd| d|j                  dd      d|j                  dd      d|j                  d      d|j                  dd      d|j                  d      d|j                  d      d|j                  d      d|j                  d      d|j                  d      d|ddd|j                  d      xs g d|j                  d      d|j                  d      d|j                  d      ||d}	t        |	|       	 |j                  j                  |	       t         5  t        j                  |       }|||d<   t                ddd       y# 1 sw Y   xY w# t        $ r!}t        j                  d| |       Y d}~yd}~ww xY w# t        $ r!}t        j                  d| |       Y d}~d}~ww xY w# 1 sw Y   yxY w# t         5  t        j                  |       }|||d<   t                ddd       w # 1 sw Y   w xY wxY w)zDMark a batch record complete and push ONE combined completion event.Nr   rb   rd   r   r   r   zVAsync delegation batch %s finished but process_registry import failed; result lost: %srC   rl   rm   r>   r?   r@   rA   rB   r7   r8   r9   r:   r;   r<   r=   Tr   live_transcriptsrp   r  )rC   rd   zNAsync delegation batch %s: failed to enqueue completion event; result lost: %s)r   r   rI   rD   r   r   r   rH   r   rp   rg   r   r   r   )
r>   r  rb   rN   r   r   r   rC   rd   r   s
             r   r  r    s    
 $m,>$ $ (x!%~!%~F|$; !$$_5DM##N3Btyy{L" 	|''r: 	 0 01G L	
 	\--.AB 	  , 	!!'* 	<##I. 	L$$Z0 	  ( 	!!'* 	& 	D  	8<<	*0b!( 	HLL);<)* 	g&+, 	!(,,/G"H-. '$1C4 X&&))--c2  	&\\-0F!#)x #%		& 	&s$ $  &3	

 	N  
3	
 	

	& 	&] 	&\\-0F!#)x #%		& 	& 	&sv   H,HH! I $'I;H!	I*II	I8I3.J 3I88J ;JK
'J>5	K
>KK
c                     t         5  t        j                         D  cg c],  } | j                         D ci c]  \  }}|dk7  s|| c}}. c}}} cddd       S c c}}w c c}}} w # 1 sw Y   yxY w)zSnapshot of async delegations (running + recently completed).

    Safe to call from any thread. Excludes the non-serialisable interrupt_fn.
    r   N)r   r   r   r   )r   kvs      r   list_async_delegationsr  C  sm    
 
 
 __&
 
  ggi?da1+>QT?

 
?

 
s3   A,A%
AAA%
A,A%
%A,,A5c                   d}t         5  t        j                         D cg c]  }|j                  d      dk(  s| }}ddd       D ],  }|j                  d      }t	        |      s 	  |        |dz  }. |rt        j                  d	||        |S c c}w # 1 sw Y   ZxY w# t
        $ r0}t        j                  d|j                  d      |       Y d}~d}~ww xY w)
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   rb   r   Nr   r   z&interrupt_all: %s interrupt failed: %sr>   z'Interrupted %d async delegation(s) (%s))	r   r   r   rI   callablerH   r   debugr   )reasoncountr   targetsfnr   s         r   interrupt_allr  O  s     E	 
(
AEE(Oy,HA
 

  
UU>"B<

 =ufML!

 
  <EE/*C s8   BBBB+B%BB"%	C.&CCc           	        | s|s|syd}t         5  t        j                         D cg c]  }|j                  d      dk(  rk|r!t	        |j                  d      xs d      |k(  sF| r!t	        |j                  d      xs d      | k(  s#|r#t	        |j                  d      xs d      |k(  r| }}ddd       D ],  }|j                  d	      }t        |      s 	  |        |d
z  }. |rt        j                  d||       |S c c}w # 1 sw Y   ZxY w# t        $ r0}t        j                  d|j                  d      |       Y d}~d}~ww xY w)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   rb   r   rA   r@   r?   rB   Nr   r   z.interrupt_for_session: %s interrupt failed: %sr>   z:Interrupted %d async delegation(s) for ending session (%s))
r   r   r   rI   r   r  rH   r   r  r   )	r?   rA   rB   r  r  r   r  r  r   s	            r   interrupt_for_sessionr  k  sM   0 3<ME	 	
(
uuX)+%#aee4J.K.Qr*RVj*jCm(<(B$C{$R%#aee4G.H.NB*OSd*d 
 
	
  
UU>"B<

 H6	
 L3
	
 	
   DEE/*C s5   DBD-DDDD	E&EEc                     t         5  t        t        j                  d       dadaddd       t        5  t
        j                          ddd       y# 1 sw Y   -xY w# 1 sw Y   yxY w)z6Test-only: clear all state and tear down the executor.NF)waitr   )r   r   shutdownr   r   r   clearr   r   r   _reset_for_testsr    sg     
 " E*	 !	"
 
  " "
 s   !AA A A))r   zsqlite3.Connection)rN   Dict[str, Any]r   r   )r>   r   r   r   r   )re   r   rf   r   r   r   )r   r   )r>   r   r   rs   )r>   r   r   r   r   rs   )r   r   r   r   r   Optional[str])r   r   r   r   r   r   )r>   r   r   zOptional[Dict[str, Any]])r   r   r   r   )r   r   )r7   r   r9   r!  r:   Optional[List[str]]r;   r   r<   r!  r?   r   rB   r!  r   Callable[[], Dict[str, Any]]rA   r   r   Optional[Callable[[], None]]r   r   r   r   )r>   r   rf   r   rb   r   r   r   )rN   r   rf   r   rb   r   r   r   )r8   z	List[str]r9   r!  r:   r"  r;   r   r<   r!  r?   r   rB   r!  r   r#  rA   r   r   r$  r   r   r>   r!  r   r   )r>   r   r  r   rb   r   r   r   )r   zList[Dict[str, Any]])r  )r  r   r   r   )r@   r@   r@   session_end)
r?   r   rA   r   rB   r   r  r   r   r   )E__doc__
__future__r   rK   loggingr*   	threadingrD   r   concurrent.futuresr   typingr   r   r   r   r	   hermes_constantsr   tools.daemon_poolr   tools.thread_contextr   	getLoggerr   r   r   r   __annotations__Lockr   r   r   r   _DEFAULT_MAX_ASYNC_CHILDRENrZ   rW   r[   rJ   r   r3   rR   rU   rM   rg   ri   r|   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r
  r  r  r  r  r  r   r   r   <module>r3     s$  !F #       1 6 6 , 6 <			8	$
 5  +/	' .! s 	  ')
# (  -  9>>*%P8^
$N	

,^6	!!(T
!!T
S
"&a+ 4 (, "159|D
|D |D "	|D
 |D |D |D %|D )|D |D /|D |D |D~",7
7
$27
<?7
	7
D (, "159#'wDwD wD "	wD
 wD wD wD %wD )wD wD /wD wD !wD wDtA&A&"0A&:=A&	A&H	
:  "	555 5 	5
 	5p	r   