
    sjul                       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m	Z	 ddl
mZmZmZ ddlmZ  ej        d          ZddZddZddZddZddZ G d d          ZdS ) u  Kanban board watcher methods for GatewayRunner.

Extracted verbatim from ``gateway/run.py`` (god-file decomposition Phase 3).
These are the background-loop methods that subscribe to kanban boards, deliver
notifications/artifacts, and drive the multi-agent dispatcher. They use only
``self`` state, so they live on a mixin that ``GatewayRunner`` inherits — the
``self._kanban_*`` call sites resolve identically via the MRO, making this a
behavior-neutral move that lifts ~1,000 LOC out of run.py.
    )annotationsNPath)AnyCallableOptional)tzgateway.runload_configCallable[[], Any]return'tuple[bool, int]'c                t   	  |             }n# t           $ r Y dS w xY wt          |t                    r|                    di           ni }t	          |                    dd                    }	 t          |                    dd          pd          }n# t          t          f$ r d}Y nw xY w|dk     rd}||fS )u  Resolve the live (enabled, per_tick) auto-decompose settings.

    Read fresh from config on every dispatcher tick (#49638) so that flipping
    ``kanban.auto_decompose: false`` to STOP runaway fan-out takes effect on the
    next tick instead of requiring a gateway restart. Auto-decompose is a
    safety toggle — a user who sees it create and launch tasks they didn't
    intend reaches for this flag to halt it, and a stale boot-captured value
    silently ignoring that change is the bug reported in #49638.

    Fails **safe**: if the config read raises, return ``(False, 3)`` — a
    transient read error must never re-enable a feature the user turned off,
    nor fall back to the burst-prone default-on behaviour. ``per_tick`` is
    clamped to ``>= 1``.
    )F   kanbanauto_decomposeTauto_decompose_per_tickr      )	Exception
isinstancedictgetboolint	TypeError
ValueError)r
   cfgkcfgenabledper_ticks        ;/home/agent/.hermes/hermes-agent/gateway/kanban_watchers.py _resolve_auto_decompose_settingsr!      s    "kmm   xx$.sD$9$9A3778R   rD488,d3344Gtxx 91==BCCz"   !||Hs   
 
/%B B+*B+r   c                 X    	 ddl m}  n# t          $ r Y dS w xY w | dt                     S )u  Return False while the global emergency stop (`hermes pause`) is engaged.

    Checked every dispatcher tick BEFORE spawning new workers so a pause takes
    effect on the next tick without a gateway restart. In-flight workers are
    never touched — this only stops NEW spawns. Fails open: if the estop
    module is unimportable, dispatch proceeds (the sentinel gate must not
    become a new crash surface for the dispatcher).
    r   check_pausedTr   )agent.estopr$   ImportErrorloggerr#   s    r    _kanban_dispatch_allowedr(   <   sS    ,,,,,,,   tt|Hf----s   	 
'tuple[Optional[object], str]'c                4   	 ddl m} n# t          $ r Y dS w xY w	 t          |           j                            dd           t          t          |           dd          }n# t          $ r Y dS w xY w ||          s|	                                 d	S |d
fS )u  Take an exclusive, non-blocking advisory lock for the sole dispatcher.

    Only one gateway process machine-wide may run the embedded kanban
    dispatcher: concurrent dispatchers double the reclaim frequency (each
    runs its own ``release_stale_claims`` → promote → dispatch loop), double
    claim-attempt events in the event log, and — with ``wal_autocheckpoint=0`` —
    concurrent manual WAL checkpoints can corrupt index pages. The
    ``dispatch_in_gateway`` config flag is the primary control; this lock is the
    backstop that survives config drift and same-profile restart races.

    Delegates to :func:`gateway.status._try_acquire_file_lock` (``fcntl`` on
    POSIX, ``msvcrt`` on Windows) so the guard is cross-platform.

    Returns ``(handle, "held")`` on success — the caller keeps the file handle
    for the process lifetime and **must** release it via
    :func:`_release_singleton_lock` when done. ``(None, "contended")`` when
    another process holds the lock (caller must NOT dispatch). ``(None,
    "unavailable")`` when locking cannot be performed (non-POSIX filesystem
    without flock, or the status.py helpers are unimportable) — caller falls
    back to config-only control.
    r   )_try_acquire_file_lock)NunavailableT)parentsexist_okza+zutf-8)encoding)N	contendedheld)
gateway.statusr+   r&   r   parentmkdiropenstrOSErrorclose)	lock_pathr+   handles      r    _acquire_singleton_lockr;   L   s    ,#9999999 # # #"""##Y$$TD$AAAc)nndW=== # # #"""#!!&)) !  6>s   	 
AA$ $
A21A2Nonec                    | dS 	 ddl m}  ||            n# t          $ r Y nw xY w	 |                                  dS # t          $ r Y dS w xY w)zQRelease a dispatcher singleton lock acquired via :func:`_acquire_singleton_lock`.Nr   )_release_file_lock)r2   r>   r   r8   )r:   r>   s     r    _release_singleton_lockr?   q   s    ~5555556""""      s    
%%? 
AAadapterr   subr   Optional[str]c                   |                     d          }t          |t                    r-dD ]*}|                     |          }|rt          |          c S +t	          | dd          }t          |          r	  |t          |                     d          pd                    }nI# t          $ r<}t                              d|                     d          |d	           Y d}~dS d}~ww xY w|rt          |          S dS )
u  Return the tenant scope (Slack workspace) a subscription's wake keys to.

    ``build_session_key()`` includes ``SessionSource.scope_id`` on platforms
    where one bot serves several isolated tenants, so a wake source must carry
    the same scope as inbound messages from that chat to resolve to the same
    session.

    The subscription's persisted ``delivery_metadata`` wins over the adapter's
    live chat → scope mapping, because it records the scope the subscription was
    created from; the mapping only covers rows that stored no metadata. ``None``
    means the chat has no scope, which is what an unscoped platform's key
    contains.
    delivery_metadata)scope_idslack_team_idteam_idscope_id_for_chatNchat_id z4kanban notifier: scope lookup failed for chat %s: %sTexc_info)	r   r   r   r6   getattrcallabler   r'   debug)r@   rA   delivery_metakeyvalueresolverresolvedexcs           r    _wake_scope_idrV      s6    GG/00M-&& "; 	" 	"C!%%c**E "5zz!!!"w 3T::H !
	xCGGI$6$6$<" = =>>HH 	 	 	LLF	""	     44444	  	!x== 4s   9-B' '
C-11C((C-c                  ^    e Zd ZdZddZddZd d!d
Z	 d"d#dZd"d$dZ	 d"d%dZ	d&dZ
ddZdS )'GatewayKanbanWatchersMixinz?Kanban watcher / notifier / dispatcher loops for GatewayRunner.r   r   c                (    t          | dd          duS )z>Return whether this gateway currently owns the singleton lock._kanban_dispatcher_lock_handleN)rM   )selfs    r    _owns_kanban_dispatcher_lockz7GatewayKanbanWatchersMixin._owns_kanban_dispatcher_lock   s    t=tDDDPP    r<   c                T    t          | dd          }d| _        t          |           dS )z>Clear notifier-visible ownership before releasing the OS lock.rZ   N)rM   rZ   r?   )r[   r:   s     r    _release_kanban_dispatcher_lockz:GatewayKanbanWatchersMixin._release_kanban_dispatcher_lock   s0    ?FF.2+'''''r]         @intervalfloatc                N   789:;<=>?@ABCDK   ddl m} 	 ddlm; n+# t          $ r t
                              d           Y dS w xY wd7d}t           di           }| _        t           d	d          @@s 	                                @@ _
        t          j        d
           d{V  d}d} j        rj	 t          j                    |k    9d:9rtt          j                    |z   }	 ddlm}  |            pi                     d          pi }t'          |                    dd                    :n# t          $ r d:Y nw xY w79:;@ fd}	t          j        |	           d{V }
|
D ]$}|d         C|d         }|                    d          }Cd         pd                                B	  |B          An8# t,          $ r+ t          j         j        C|d         |           d{V  Y w xY wC                    d          pdD                     ADpd          ??at
                              dBCd                    t          j         j        C|d         |                    dd          |           d{V  |r|j        nCd         dd         }|rd| dnd}Cd         Cd         Cd         C                    d           pdf}C                    d!          pd"}|d#v }|d$k    }d}|d%         D ]`}|j        }|r|j        r|j        nd}|rd&| d'nd}|d(k    rd}d}|j        r4|j                            d)          rt?          |j        d)                   }|rJ|                                 !                                }|r|d         dd*         n	|dd*         }d+| }|}n\|rZ|j"        rS|j"                                         !                                }|r|d         dd,         n|j"        dd,         }d+| }|}d-| | d.Cd          d/| | }n|d0k    r]d}|j        r?|j                            d1          r%d2t?          |j        d1                   dd,          }d3| | d.Cd          d4| }n|d5k    r]d} |j        r?|j                            d6          r%d+t?          |j        d6                   dd*          } d7| | d.Cd          d8|  }n|d9k    rd7| | d.Cd          d:}n|d;k    rSd}!|j        r4|j                            d<          rt'          |j        d<                   }!d=| | d.Cd          d>|! d?}nG|d@k    rQd}"|j        r4|j                            d@          rt?          |j        d@                   }"dA| | d.Cd          dB|" }n|dCk    r^d}|j        r?|j                            d)          r%d+t?          |j        d)                   dd*          }dD| | d.Cd          dE| | }n|dFk    rd}d}#|j        rY|j                            d1          r%d2t?          |j        d1                   dd,          }|j                            dG          }#|#rdH|# dInd}$dJ| | d.Cd          dK|$ | }nC                    dL          }%tG          |%tH                    rtI          |%          ni }&C                    d           r |&                    d           sCd          |&d <   ddMl%m&}'  |'?          s&|r$t
                              dNBCd                    P|sT	 ?'                    Cd         ||&O           d{V }(t          |(dPdQ          dRu r#tQ          dSt          |(d6d          pdT           t
                              dU|Cd         BCd         |           |d(k    rp	  )                    ?Cd         |&t          |dVd          |W           d{V  n9# t          $ r,})t
                              dXCd         |)           Y d})~)nd})~)ww xY w|*                    |d           h# t          $ r}*|                    |d          dYz   }+|+||<   t
                              dZCd         B|+||*           |+|k    r[t
                              d[Cd         B|+           t          j         j+        C|           d{V  |*                    |d           n=t          j         j        C|d         |                    dd          |           d{V  Y d}*~* n4d}*~*ww xY w|o
|j,        d\k    },d]8|r8fd^|d%         D             nt[                      >ddMl%m&}-  |-?          }.d<d=>r1|.rt          |d_d          pd<nCd         pt          |d_d          pd<>r^|r|j        nCd         dd         }/|r|j        nd}0g }1d(>v r"|1.                    t_          d`                     d5>v r"|1.                    t_          da                     d9>v r"|1.                    t_          db                     d;>v r"|1.                    t_          dc                     d0>v r"|1.                    t_          dd                     t_          de          0                    |1          pt_          df          }2t_          dgCd         |2|/|0|h          =|r=d+t_          di|j          z   z  ==dkt_          dl          z   z  =|.s_>r\<rYddml%m1}3 	  |3?=<n           d{V  t
          2                    doCd         BCd         Dpdp>           |*                    |d           n# t          $ r}4|                    |d          dYz   }+|+||<   t
                              dqCd         |+||4dQr           |+|k    r[t
                              dsCd         B|+           t          j         j+        C|           d{V  |*                    |d           n=t          j         j        C|d         |                    dd          |           d{V  Y d}4~4.d}4~4ww xY wdz<=>?ABCDfdv}5|.r(|s%>r"	  |5             d{V  |*                    |d           n# t          $ r}4|                    |d          dYz   }+|+||<   t
                              dwCd         |+||4dQr           |+|k    r[t
                              dsCd         B|+           t          j         j+        C|           d{V  |*                    |d           n=t          j         j        C|d         |                    dd          |           d{V  Y d}4~4fd}4~4ww xY wt          j         j        C|d         |           d{V  |.s|*                    |d           |.rQ|rO>rM	  |5             d{V  n;# t          $ r.}4t
                              dxCd         |4dQr           Y d}4~4nd}4~4ww xY w|,r!t          j         j+        C|           d{V  &n2# t          $ r%}*t
                              dy|*           Y d}*~*nd}*~*ww xY wtg          t'          ti          dY|                              D ]&}6 j        s dS t          j        dY           d{V  ' j        hdS dS ){a  Poll ``kanban_notify_subs`` and deliver terminal events to users.

        For each subscription row, fetches ``task_events`` newer than the
        stored cursor with kind in the terminal set (``completed``,
        ``blocked``, ``gave_up``, ``crashed``, ``timed_out``,
        ``review_requested``, ``block_loop_detected``). Sends one
        message per new event to ``(platform, chat_id, thread_id)``,
        then advances the cursor. The subscription is removed only when the
        task is ``archived``. A ``done`` task can be reopened for review or
        continuation, so its subscription and origin-session ownership must
        survive completion. Cursor advancement prevents old events replaying
        when that happens.

        Runs in the gateway event loop; all SQLite work is pushed to a
        thread via ``asyncio.to_thread`` so the loop never blocks on the
        WAL lock. Failures in one tick don't stop subsequent ticks.

        **Multi-board:** iterates every board discovered on disk per
        tick. Each gateway polls only subscriptions owned by profiles whose
        adapters it hosts. The dispatch-owning gateway also handles legacy
        subscriptions without a profile stamp.
        r   )Platform	kanban_dbz<kanban notifier: kanban_db not importable; notifier disabledN)
	completedblockedgave_upcrashed	timed_outstatusarchived	unblockedblock_loop_detectedreview_requested   _kanban_sub_fail_counts_kanban_notifier_profile   g      @           r
   r   done_sub_retention_daysc                 B   g }                                  }h}|                    d t          di           D                        d j                                        D             }t          di                                           D ]3}|                    d |                                D                        4|st                              d           | S 	                     d          }n+# t          $ r 
                    j                  g}Y nw xY wt                      }|D ]i}|                    d          pj        }|                    d	          }		 |	r@t          t          |	                                                                                    n3t                              |                                                    }
n# t          $ r d
| }
Y nw xY w|
|v rt                              d||
           |                    |
           	                     |||          dk    r+t                              d|t+          |                     ?n3# t          $ r&}t                              d||           Y d }~nd }~ww xY w	                     |          }n4# t          $ r'}t                              d||           Y d }~d }~ww xY w	 rk	                     |          }|rt                              d||           n3# t          $ r&}t                              d||           Y d }~nd }~ww xY w                    |||          }|st                              d|           |D ]}	 |                    d          pd }|r]|k    rWt          di                               |          }|s1t                              d|                    d          |           z|                    d          pd                                }||vr2t                              d|                    d          |pd           ى                    ||d         |d         |d         |                    d          pd           \  }}}|s#                    ||d                   }t                              d!t;          |          |d         |||           |                     ||||||d"           # t          $ r;}t                              d#|                    d          ||           Y d }~d }~ww xY w	 |                                  R# |                                  w xY w| S )$Nc              3     K   | ]F}t          |                                          #t          |                                          V  Gd S N)r6   strip).0profiles     r    	<genexpr>zXGatewayKanbanWatchersMixin._kanban_notifier_watcher.<locals>._collect.<locals>.<genexpr>#  s`       - -#w<<--//-G**,,- - - - - -r]   _profile_adaptersc           	     n    h | ]2}t          |d t          |                                                    3S )rR   rM   r6   lowerr}   platforms     r    	<setcomp>zXGatewayKanbanWatchersMixin._kanban_notifier_watcher.<locals>._collect.<locals>.<setcomp>(  sF     ( ( ($  '3x==AAGGII( ( (r]   c              3  v   K   | ]4}t          |d t          |                                                    V  5dS )rR   Nr   r   s     r    r   zXGatewayKanbanWatchersMixin._kanban_notifier_watcher.<locals>._collect.<locals>.<genexpr>:  sT       0 0 ( $Hgs8}}EEKKMM0 0 0 0 0 0r]   z5kanban notifier: no connected adapters; skipping tickFinclude_archivedslugdb_pathzslug:z;kanban notifier: skipping duplicate board slug %s for DB %s)boardnotifier_profilesinclude_unownedr   zIkanban notifier: board %s has no subscriptions owned by %s; skipping openzekanban notifier: read-only subscription probe failed for board %s (%s); falling back to writable openr   z)kanban notifier: cannot open board %s: %s)max_age_dayszVkanban notifier: purged %d stale done-task subscription(s) on board %s (retention %dd)z5kanban notifier: stale-sub GC failed for board %s: %s)r   r   z.kanban notifier: board %s has no subscriptionsnotifier_profilezlkanban notifier: subscription for %s owned by profile %s; current profile %s has no adapter for it, skippingtask_idr   rJ   zIkanban notifier: subscription for %s on %s skipped; adapter not connectedz	<missing>rI   	thread_id)r   r   rI   r   kindsuF   kanban notifier: claimed %d event(s) for %s on board %s cursor %s→%s)rA   
old_cursorcursoreventstaskr   z;kanban notifier: subscription for %s on board %s failed: %s)!r\   updaterM   adapterskeysvaluesr'   rO   list_boardsr   read_board_metadataDEFAULT_BOARDsetr   r6   r   
expanduserresolvekanban_db_pathaddcount_notify_subssortedconnectpurge_stale_done_notify_subsinfolist_notify_subsr   claim_unseen_events_for_subget_tasklenappendwarningr8   )
deliveriesr   r   active_platforms_profile_adapter_mapboardsseen_db_paths
board_metar   r   resolved_db_pathrU   conn_purged_gc_excsubsrA   owner_profile_owner_adaptersr   r   r   r   r   sub_excTERMINAL_KINDS_gc_due_gc_retention_days_kbr   r[   s                            r    _collectzEGatewayKanbanWatchersMixin._kanban_notifier_watcher.<locals>._collect  sU   -/J&*&G&G&I&IO)9(:%%,, - -'.t5H"'M'M- - -   
( ((,(:(:(<(<( ( ($" 18>QSU0V0V0]0]0_0_  ,(// 0 0,@,E,E,G,G0 0 0     , *%\]]]))N!$%!H!H$ N N N"%"9"9#:K"L"L!MN.1eeM&, G) G)
)~~f55J9J",..";";>\c  0Qs4==3K3K3M3M3U3U3W3W/X/X/Xilmpmm  AE  nF  nF  nN  nN  nP  nP  jQ  jQ,,( > > >/=t~~,,,>+}<<"LL ] $&6   %%))*:;;;"44&*2C0?  5     "#	 #  #
 !'$o$(&1B*C*C!" !" !" !) #  )   "LL!S $c       %#&;;T;#:#:DD( % % %"LL)TVZ\_```$HHHH%\)& &!&.1.N.N(,5G /O /& /&G (/ %*(. -E,3T;M)* )* )* (1 !& !& !&$*LL(_(,g%& %& %& %& %& %& %& %&!&" $'#7#7 $2C0? $8 $ $D
 $( e &-]_c d d d'+ 0& 0&/!&47GG<N4O4O4WSWM'4 %5JZ9Z9Z:A$H[]_:`:`:d:der:s:s/> )5,2LL 1_03	0B0BMSc-. -. -. -503
0C0C0Ir/P/P/R/RH'/7G'G'G(.,w,/GGI,>,>@WK)* )* )* )1ADA`A`(,03I14Z03I25''+2F2F2L".< Ba B& B&$>J ,2 %1(0+.<<c)n+M+MD$*LL(p(+FS^T:W]%& %& %& %/$5$5/26@282804157& 7& %' %' %' %' (1 !& !& !& %+NN(e(+	(:(:D'%& %& %& %& %& %& %& %&	!&S0&d !JJLLLLDJJLLLL%%s   "C9 9%D! D!)A6G  G21G2,AI44
J$>JJ$(J??
K0	K++K04V76L.-V.
M8MVM=VA5T'VAT'0V1AT'9V;A*T'%V'
U,10U'!V'U,,VVrA   r   r   r   rJ   r   r   zPkanban notifier: adapter %s disconnected before delivery for %s; rewinding claimr   r   x   [z] rI   r   delivery_modenotify)znotify+wakewaker   r   @ rg   summary   
   u   ✔ zKanban u
    done — rh   reasonz: u   ⏸ z blockedri   erroru   ✖ z& gave up after repeated spawn failuresrj   z1 worker crashed (pid gone); dispatcher will retryrk   limit_secondsu   ⏱ z timed out (max_runtime=zs); will retryrl   u   🔄 u    → rp   u   👀 u    ready for review — ro   recurrencesz
 (blocked zx for the same cause)u   🛑 u,    routed to TRIAGE — needs a human decisionrD   )adapter_supports_pushzmkanban notifier: adapter %s has no push channel; skipping text ping for %s, relying on wake self-post instead)metadatasuccessTFz!adapter send() reported failure: zunknown errorz?kanban notifier: delivered %s event for %s to %s/%s on board %spayload)r@   rI   r   event_payloadr   z4kanban notifier: artifact delivery for %s failed: %sr   z=kanban notifier: send failed for %s on %s (attempt %d/%d): %szRkanban notifier: dropping subscription %s on %s after %d consecutive send failuresrm   )rg   ri   rj   rk   rh   c                0    h | ]}|j         v |j         S  )kind)r}   ev_WAKE_KINDSs     r    r   zFGatewayKanbanWatchersMixin._kanban_notifier_watcher.<locals>.<setcomp>  s'    UUUbg>T>TRW>T>T>Tr]   
session_idzgateway.kanban.wake.completedzgateway.kanban.wake.gave_upzgateway.kanban.wake.crashedzgateway.kanban.wake.timed_outzgateway.kanban.wake.blockedz!gateway.kanban.wake.status_joinerz"gateway.kanban.wake.status_defaultzgateway.kanban.wake.message)r   rl   titleassigneer   zgateway.kanban.wake.handoff)r   z

zgateway.kanban.wake.guidancedeliver_wake)textr   @kanban notifier: woke agent for %s on %s/%s profile=%s events=%sdefaultzAkanban notifier: wake self-post failed for %s (attempt %d/%d): %srK   zRkanban notifier: dropping subscription %s on %s after %d consecutive wake failuresr   r<   c                   K   ddl m}  ddlm} t	                              d          pd                                          }|s`                    d          }t          |t                    r6t	          |                    d          pd                                          }|pd} | 	d         |                    d	          pd
                    d                              d          pd
t                              } ||           d
{V  t                              dd         
d         pd           d
S )ar  Wake the creator session behind a push adapter.

                            Shared by the wake-only (pre-advance, delivery)
                            and notify+wake (post-advance, best-effort)
                            branches below; raises on failure so the caller
                            decides whether to rewind or merely log.
                            r   )SessionSourcer   	chat_typerJ   rD   grouprI   r   Nuser_iduser_id_alt)r   rI   r   r   r   r   r~   rE   )r   r   sourcer   r   r   )gateway.sessionr   gateway.waker   r6   r   r|   r   r   rV   r'   r   )r   r   
_chat_type_delivery_meta_source_session_key_synth_wake_kindsr@   platplatform_strrA   sub_profiles        r    
_push_wakezGGatewayKanbanWatchersMixin._kanban_notifier_watcher.<locals>._push_waked  s      FEEEEEAAAAAA *-SWW[-A-A-GR)H)H)N)N)P)PJ#- .149L1M1M#-nd#C#C !.14(6(:(:;(G(G(M22& 2&&+egg %/ *4)>wJ&3m)-(+I*4*-''+*>*>*F$(+	(:(:,/GGM,B,B(3(;t)7)E)E	' 	' 	'G #/, '%++7'.	# # #        #KK b #Ic)nkNf]fhs    r]   zEkanban notifier: wake-only delivery failed for %s (attempt %d/%d): %sz3kanban notifier: wakeup injection failed for %s: %szkanban notifier tick failed: %sr   r<   )5gateway.configrd   
hermes_clirf   r   r'   r   rM   rr   _active_profile_namers   asynciosleep_runningtime	monotonichermes_cli.configr
   r   r   	to_threadr   r   _kanban_advance_authorization_adapterrO   _kanban_rewindr   r   r   r   r6   r|   
splitlinesresultr   r   r   r   sendRuntimeError_deliver_kanban_artifactspop_kanban_unsubrl   r   r   r	   joinr   r   rangemax)Er[   ra   	_PlatformMAX_SEND_FAILURESsub_fail_counts_GC_INTERVAL_SECONDS_gc_next_at	_load_cfg_kanban_cfgr   r   dr   
board_slugr   	board_tagsub_keymode
wake_agentsend_passivewake_handoffr   r   whotaghandoffpayload_summarylineshrmsgr   errlimit
new_statusr   rcrD   r   r   	_send_resart_excrU   failstask_terminal_adapter_push_ok_is_push_adapter_title	_assignee_parts_statusr   _wk_errr   _r   r   r   r   r   r   r   r   r@   r   r   r   rA   r   sE   `                                                      @@@@@@@@@@@@@@r    _kanban_notifier_watcherz3GatewayKanbanWatchersMixin._kanban_notifier_watcher   sg     8 	988888	3333333 	 	 	NNYZZZFF	 c2 ,3+R-
 -
 (7$"4)CTJJ 	=#88::,<D) mA  &m g	'aG.**k9%'" 0"&."2"25I"IK
0NNNNNN'0y{{'8b&=&=h&G&G&M2-0'OO,ErJJ. .** % 0 0 0 .0***0
u& u& u& u& u& u& u& u& u& u&n $+#4X#>#>>>>>>>
# V VAE(CV9D!"wJ$'
O$9r#@#@#B#BL!(y66% ! ! ! &/ 0#q{J         !! #&''*<"="="CK #99$@StTTGn(#i.   &/ /hKEE,22&         !+/CTZZS^TcTJE6@ H 2J 2 2 2 2bI IJI(<(<(BG 77?33?xD!%)@!@J#'6>L
 $&Lk ^ ^!w 15PPt}}D,/7j#jjjjR;.. ')G.2O!z MbjnnY.G.G M25bj6K2L2L. 	1(7(=(=(?(?(J(J(L(L6;$VE!HTcTNNQURUQUAV*2q((/0!% 1$+ 1(,(9(9(;(;(F(F(H(H6;$RE!HTcTNNTcTAR*2q((/0!9y !9# !9 !9c)n !9 !9(-!9/6!9 !9  C "Y..%'F!z PbjnnX.F.F P)Oc"*X2F.G.G.M)O)O"`"`C"`"`I"`"`X^"`"`CC!Y.."$C!z LbjnnW.E.E L&K3rz'/B+C+CDSD+I&K&K!Fy !F# !F !Fc)n !F !F@C!F !F  C "Y..!Ey !E# !E !Ec)n !E !E !E  C "[00$%E!z Ibjnn_.M.M I(+BJ,G(H(H!Fy !F# !F !Fc)n !F !F05!F !F !F  C "X--)+J!z GbjnnX.F.F G-0H1E-F-F
"b)"bS"b"bY"b"bV`"b"bCC!%777 ')G!z RbjnnY.G.G R*Qs2:i3H/I/I$3$/O*Q*Q!9	 !93 !9 !9s9~ !9 !9(-!9/6!9 !9  C "%::: &(F*.K!z L#%:>>(#;#; !T-S#bj6J2K2KDSD2Q-S-SF.0jnn].K.KT_!g!Pk!P!P!P!PegB!K	 !K3 !K !Ks9~ !K !K>@!KBH!K !K  C %,/GG4G,H,H)  **;TBB$D!2333!# ! 77;// E[8Q8Q E474DH[1 GFFFFF44W== %* %"LL!< !-c)n	   %+ % %I".5ll #Ih /; / / ) ) ) ) ) )I  'y)TBBeKK&2%^'.y'4'H'H'[O%^ %^'" '" !" #LL a $c)nlC	NT^    ${22!&*.*H*H0703I196=b)T6R6R-1 +I +& +& %& %& %& %& %& %& %& %& (1 !& !& !&$*LL(^(+I%& %& %& %& %& %& %& %&!& ,//>>>>( " " "$3$7$7$C$Ca$GE7<OG4"NN!6 #Ie 13	    %(999 &%R$'	NL%!" !" !"
 '.&78JCQ[&\&\ \ \ \ \ \ \ \ / 3 3GT B B B B&-&7$($7$'$%hK$%EE,$:$:$.'" '" !" !" !" !" !" !" !" "EEEEE9"\ )-(J
1J&a  *'UUUUq{UUUU!$ $
 [ZZZZZ+;+;G+D+D(')!#& "/ "/6t\4/P/P/VTV %(	N %*'.t\4'H'H%*') !-
 ' 48&Ldjjc)ndsd%SF9=(E2I%'F*k996==KjIkIk;l;l;l(K77qIfGgGg9h9h9h(K77qIfGgGg9h9h9h*k996==KjIkIk;l;l;l(K77qIfGgGg9h9h9h&'(K&L&L&Q&QRX&Y&Y  'E]^  `D  ^E  ^EG%& =(+I'.&,)2&0& & &F  , " &$$A,82" 2" 2" +" !" #fq >0 0 ' F  0 ,)K ,)L ,) BAAAAA')&2l$+)//;'" '" '" !" !" !" !" !" !" !"
 !'$f$'	NL#i.R]Rjajlw!" !" !" !0 3 3GT B B B B#, ) ) )(7(;(;GQ(G(G!(K;@ 8 &%A$'	NE$5w	 !/ !" !" !" $),=#=#=$*NN)V(+Ie%& %& %&
 +2*;D<NPSU_*`*`$`$`$`$`$`$`$`$3$7$7$F$F$F$F
 +2*;(,(;(+()(()lA(>(>(2+& +& %& %& %& %& %& %& %& !)9)<8 8 8 8 8 8 8 8 8 8 8 8 8t , ()L ()[ ())&0jll 2 2 2 2 2 2 2 / 3 3GT B B B B#, ) ) )(7(;(;GQ(G(G!(K;@ 8 &%A$'	NE$5w	 !/ !" !" !" $),=#=#=$*NN)V(+Ie%& %& %&
 +2*;D<NPSU_*`*`$`$`$`$`$`$`$`$3$7$7$F$F$F$F
 +2*;(,(;(+()(()lA(>(>(2+& +& %& %& %& %& %& %& %& !)9)F &/ 0#q{J          0 ? ,//>>> , " " ""&0jll 2 2 2 2 2 2 2 2#, 
" 
" 
" !'$Y$'	NGd !/ !" !" !" !" !" !" !" !"
" ) ")"3 $ 2C# #       iVn  G G G@#FFFFFFFFG 3s1h//0011 ' '} FFmA&&&&&&&&&&O m g	' g	' g	' g	' g	'sK    $AA<1z .A
D9 8z 9Ez EA-z 6Gz 2G74z 6G77U$z Ba/(5`a/
a("a
a/aa/-z /
e&9C!e!z !e&&G.z An-,z -
r$7C"rz r$$z >&s%$z %
w/C"wz wA	z &x76z 7
y/$y*%z *y//(z 
{"{{NrA   r   r   r   r   rB   c           	        ddl m} |                    |          }	 |                    ||d         |d         |d         |                    d          pd|	           |                                 d
S # |                                 w xY w)zSync helper: advance a subscription's cursor. Runs in to_thread.

        ``board`` scopes the DB connection to the board that owns this
        subscription. Unsub cursors in one board can't touch another's.
        r   re   r   r   r   rI   r   rJ   )r   r   rI   r   
new_cursorN)r   rf   r   advance_notify_cursorr   r8   )r[   rA   r   r   r   r   s         r    r  z*GatewayKanbanWatchersMixin._kanban_advance  s     	0/////{{{''
	%%IZI''+..4"! &    JJLLLLLDJJLLLLs   AA6 6Bc           	        ddl m} |                    |          }	 |                    ||d         |d         |d         |                    d          pd	           |                                 d S # |                                 w xY w)
Nr   re   r   r   r   rI   r   rJ   )r   r   rI   r   )r   rf   r   remove_notify_subr   r8   )r[   rA   r   r   r   s        r    r  z(GatewayKanbanWatchersMixin._kanban_unsub  s    //////{{{''		!!IZI''+..4" "    JJLLLLLDJJLLLLs   AA5 5Bclaimed_cursorr   c           	         ddl m} |                    |          }	 |                    ||d         |d         |d         |                    d          pd||	           |                                 d
S # |                                 w xY w)zCSync helper: undo a claimed notification cursor after send failure.r   re   r   r   r   rI   r   rJ   )r   r   rI   r   r<  r   N)r   rf   r   rewind_notify_cursorr   r8   )r[   rA   r<  r   r   r   r   s          r    r  z)GatewayKanbanWatchersMixin._kanban_rewind  s     	0/////{{{''	$$IZI''+..4"-% %    JJLLLLLDJJLLLLs   AA7 7BrI   r6   r   r   Optional[dict]c                 K   ddl m g t                      dfd}t          |t                    r|                    d          }t          |t          t          f          r%|D ]"}t          |t                    r ||           #|                    d	          }	t          |	t                    r*|	r(|	                    |	          \  }
}|
D ]} ||           |Mt          |dd
          r<t          |j                  }|	                    |          \  }
}|
D ]} ||           sd
S ddlm} |                              sd
S h dh d}ddlm fdD             }fdD             }|r`	 fd|D             }|                    |||           d
{V  n2# t$          $ r%}t&                              d|           Y d
}~nd
}~ww xY w|D ]} |          j                                        }	 ||v r|                    |||           d
{V  n|                    |||           d
{V  g# t$          $ r&}t&                              d||           Y d
}~d
}~ww xY wd
S )u  Upload artifact files referenced by a completed kanban task.

        Workers passing ``kanban_complete(artifacts=[...])`` ship absolute
        file paths through the completion event so downstream humans get
        the deliverable as a native upload instead of a path printed in
        chat.

        Sources scanned, in priority order:
          1. ``event_payload['artifacts']`` (explicit list — preferred)
          2. ``event_payload['summary']`` (truncated first line)
          3. ``task.result`` (legacy fallback)

        Files are deduplicated, missing files are silently skipped (the
        path may have been mentioned for reference only), and delivery
        errors are logged but do not break the notifier loop.
        r   r   pathr6   r   r<   c                    | sd S t           j                            |           }|v rd S t           j                            |          sd S                     |                               |           d S r{   )osrA  r   isfiler   r   )rA  expanded
candidatesseens     r    _addzBGatewayKanbanWatchersMixin._deliver_kanban_artifacts.<locals>._addO  s|     w))$//H47>>(++ HHXh'''''r]   	artifactsr   Nr  )BasePlatformAdapter>   .gif.jpg.png.jpeg.webp>   .3gp.avi.mkv.mov.mp4.webm)quotec                \    g | ](} |          j                                         v &|)S r   suffixr   r}   p_IMAGE_EXTS_Paths     r    
<listcomp>zHGatewayKanbanWatchersMixin._deliver_kanban_artifacts.<locals>.<listcomp>  s9    WWWQa0E0E0G0G;0V0Vq0V0V0Vr]   c                \    g | ](} |          j                                         v&|)S r   rX  rZ  s     r    r^  zHGatewayKanbanWatchersMixin._deliver_kanban_artifacts.<locals>.<listcomp>  s9    [[[Qa0E0E0G0G{0Z0Zq0Z0Z0Zr]   c                0    g | ]}d  |           dfS )zfile://rJ   r   )r}   r[  _quotes     r    r^  zHGatewayKanbanWatchersMixin._deliver_kanban_artifacts.<locals>.<listcomp>  s/    JJJ/FF1II//4JJJr]   )rI   imagesr   z.kanban notifier: image batch upload failed: %s)rI   
video_pathr   )rI   	file_pathr   z0kanban notifier: artifact upload (%s) failed: %s)rA  r6   r   r<   )pathlibr   r   r   r   r   listtupler6   extract_local_filesrM   r  gateway.platforms.baserJ  filter_local_delivery_pathsurllib.parserV  send_multiple_imagesr   r'   r   rY  r   
send_videosend_document)r[   r@   rI   r   r   r   rH  rawitemr   pathsr5  r[  result_textrJ  _VIDEO_EXTSimage_pathsother_pathsbatchrU   rA  extr\  r]  ra  rF  rG  s                         @@@@@r    r	  z4GatewayKanbanWatchersMixin._deliver_kanban_artifacts1  s     2 	*))))) "
		( 		( 		( 		( 		( 		( 		( mT** 	##K00C#e}-- # # #D!$,, #T


 $''	22G'3'' G "66w??q  ADGGGG h = =dk**K22;??HE1  Q 	F>>>>>>(DDZPP
 	F@@@GGG000000 XWWWW*WWW[[[[[*[[[ 		JJJJkJJJ22#EH 3              Dc       
   	 	D%++$**,,C+%%!,, 'D8 -           "// '4( 0             F#       	 	s1   ,F2 2
G!<GG!AI
I=I88I=c                    !"#$%&'()*+,K   	 ddl m# n+# t          $ r t                              d           Y dS w xY wt
          j                            dd                                          	                                }|dv rt          
                    d           dS 	  #            }n3# t          $ r&}t                              d	|           Y d}~dS d}~ww xY wt          |t                    r|                    d
i           ni }|                    dd          st          
                    d           dS 	 ddlm" n+# t          $ r t                              d           Y dS w xY wd| _        "                                d
z  dz  }t#          |          \  }}|dk    rt          
                    d|           dS |dk    r#|| _        t          
                    d|           nt                              d|           	 t%          |                    dd          pd          }nG# t&          t(          f$ r3 t                              d|                    d                     d}Y nw xY wt+          |d          }|                    dd          **t          
                    d*           |                    dd          }	d(|		 t-          |	          ((dk     rt                              d|	           d(nPt          
                    d (           n4# t(          t&          f$ r  t                              d!|	           d(Y nw xY w"                    (          }
(|
t          
                    d"|
           |
(|                    d#"j                  }	 t-          |          'n?# t(          t&          f$ r+ t                              d$|"j                   "j        'Y nw xY w'dk     r(t                              d%|"j                   "j        '|                    d&d          }	 t-          |pd          ,n4# t(          t&          f$ r  t                              d'|           d,Y nw xY wt3          |                    d(d                    +|                    d)          pd                                pd%%rt          
                    d*%           |                    d+d          }d)|	 t-          |          ))dk     rt                              d,|           d)nPt          
                    d-)           n4# t(          t&          f$ r  t                              d.|           d)Y nw xY wt5          j        d/           d{V  d0}d}d}d1i &dP"fd6 dQ"fd:!dR !"%&'()*+,fd<$dS"$fd>}dT"fd?}dU#fdA}dV"fdD}t          
                    dE|           | j        r	 t5          j        "j                   d{V }|r)t          
                    dFt?          |          |           n*# t          $ r t                               dG           Y nw xY w	 tC                      sdH}d}nC |            \  }}|rt5          j        ||           d{V  t5          j        |           d{V }dH}|pg D ]\  }}|tE          |dId          rd}t          
                    dJ|t?          |j#                  |j$        tK          |j&        dK          rt?          |j&                  ndtK          |j'        dK          rt?          |j'                  nd|j(        tK          |j)        dK          rt?          |j)                  nd           t5          j        |           d{V }|r|s|dz  }nd}||k    rFt-          tU          j*                              }||z
  d1k    rt                              dL|           |}ng# t4          j+        $ r0 t          ,                    dM           | -                                  t          $ r t                               dN           Y nw xY wdO}||k     rD| j        r=t5          j        t]          d||z
                       d{V  |dz  }||k     r| j        =| j        | -                                 dS )Wu  Embedded kanban dispatcher — one tick every `dispatch_interval_seconds`.

        Gated by `kanban.dispatch_in_gateway` in config.yaml (default True).
        When true, the gateway hosts the single dispatcher for this profile:
        no separate `hermes kanban daemon` process needed. When false, the
        loop exits immediately and an external daemon is expected.

        Each tick calls :func:`kanban_db.dispatch_once` inside
        ``asyncio.to_thread`` so the SQLite WAL lock never blocks the
        event loop. Failures in one tick don't stop subsequent ticks —
        same pattern as `_kanban_notifier_watcher`.

        Shutdown: the loop checks ``self._running`` between ticks; gateway
        stop() flips it to False and cancels pending tasks, and the
        in-flight ``to_thread`` returns on its own after the current
        ``dispatch_once`` call finishes (typically <1ms on an idle board).
        r   rw   z6kanban dispatcher: config loader unavailable; disabledN!HERMES_KANBAN_DISPATCH_IN_GATEWAYrJ   >   0noofffalsezEkanban dispatcher: disabled via HERMES_KANBAN_DISPATCH_IN_GATEWAY envz4kanban dispatcher: cannot load config (%s); disabledr   dispatch_in_gatewayTzGkanban dispatcher: disabled via config kanban.dispatch_in_gateway=falsere   z@kanban dispatcher: kanban_db not importable; dispatcher disabledz.dispatcher.lockr0   zjkanban dispatcher: another gateway already holds the dispatcher lock (%s); this gateway will NOT dispatch.r1   z9kanban dispatcher: holding singleton dispatcher lock (%s)zWkanban dispatcher: advisory lock unavailable at %s; proceeding on config control alone.dispatch_interval_seconds<   zIkanban dispatcher: invalid dispatch_interval_seconds=%r, using default 60g      N@g      ?	max_spawnzkanban dispatcher: max_spawn=%smax_in_progressr   zAkanban dispatcher: kanban.max_in_progress=%r is below 1; ignoringz%kanban dispatcher: max_in_progress=%sz>kanban dispatcher: invalid kanban.max_in_progress=%r; ignoringzkanban dispatcher: kanban.max_in_progress unset; using memory-derived default max_in_progress=%d (set kanban.max_in_progress in config.yaml to override)failure_limitzDkanban dispatcher: invalid kanban.failure_limit=%r; using default %dzGkanban dispatcher: kanban.failure_limit=%r is below 1; using default %ddispatch_stale_timeout_secondsz^kanban dispatcher: invalid kanban.dispatch_stale_timeout_seconds=%r; disabling stale detectionreconcile_orphansdefault_assigneezZkanban dispatcher: default_assignee=%r (unassigned ready tasks will route to this profile)max_in_progress_per_profilezMkanban dispatcher: kanban.max_in_progress_per_profile=%r is below 1; ignoringz1kanban dispatcher: max_in_progress_per_profile=%dzJkanban dispatcher: invalid kanban.max_in_progress_per_profile=%r; ignoringrt      i,  r   r6   r   "tuple[str, int | None, int | None]c                J                        |           }	 t          |                                                                          }n# t          $ r t          |          }Y nw xY w	 |                                }n# t          $ r |d d fcY S w xY w||j        |j        fS r{   )	r   r6   r   r   r   statr7   st_mtime_nsst_size)r   rA  rT   r  r   s       r    _board_db_fingerprintzTGatewayKanbanWatchersMixin._kanban_dispatcher_watcher.<locals>._board_db_fingerprint  s    %%d++D%t0088::;; % % %t99%.yy{{ . . . $----.d.==s#   3A A('A(,B BBrU   r   r   c                    t          dd           }|t          | |          rdS t          | t          j                  sdS t	          |                                           }d|v pd|v S )NKanbanDbCorruptErrorTFzfile is not a databasez database disk image is malformed)rM   r   sqlite3DatabaseErrorr6   r   )rU   corrupt_guard_errorr%  r   s      r    _is_corrupt_board_db_errorzYGatewayKanbanWatchersMixin._kanban_dispatcher_watcher.<locals>._is_corrupt_board_db_error  sz    ")#/Et"L"L".:cCV3W3W.tc7#899 uc((..""C(C/ =5<r]   'Optional[object]'c                   d} 	|           }                     |           }|}|\  }}t          j                    |z
  }||k    r|k     rdS ||k    rt                              d| |           nt                              d|                                | d           	                     |           }                    || 	  	        |&	 |                                 S # t          $ r Y S w xY wS # t          j        $ r} 
|          rj|t          j                    f| <   t                              d| |d                    Y d}~|(	 |                                 dS # t          $ r Y dS w xY wdS t                              d|            Y d}~|(	 |                                 dS # t          $ r Y dS w xY wdS d}~wt          $ r} 
|          rj|t          j                    f| <   t                              d| |d                    Y d}~|(	 |                                 dS # t          $ r Y dS w xY wdS t                              d|            Y d}~|(	 |                                 dS # t          $ r Y dS w xY wdS d}~ww xY w# |&	 |                                 w # t          $ r Y w w xY ww xY w)	a  Run one dispatch_once for a specific board.

            Runs in a worker thread via `asyncio.to_thread`. `board=slug`
            is passed through `dispatch_once` so `resolve_workspace` and
            `_default_spawn` see the right paths. The per-board DB is
            opened explicitly so concurrent boards never share a
            connection handle or accidentally claim across each other.
            Nzdkanban dispatcher: board %s database fingerprint unchanged after %.0fs quarantine; retrying dispatchz?kanban dispatcher: board %s database changed; retrying dispatchr   )r   r  r  r  stale_timeout_secondsr  r  r  a	  kanban dispatcher: board %s database %s is not a valid SQLite database; pausing dispatch for this board until the file changes, the gateway restarts, or the quarantine timer expires. Move or restore the file, then run `hermes kanban init` if you need a fresh board.r   z*kanban dispatcher: tick failed on board %s)r   r   r   r'   r   r
  r   dispatch_oncer8   r   r  r  r   	exception)r   r   fingerprintdisabled_entrydisabled_fingerprintdisabled_atagerU   !CORRUPT_BOARD_RETRY_AFTER_SECONDSr  r  r   r  disabled_corrupt_boardsr  r  r  r  r  r  s           r    _tick_once_for_boardzSGatewayKanbanWatchersMixin._kanban_dispatcher_watcher.<locals>._tick_once_for_board  s    D//55K488>>N)4B1$kn&&4(K77???4';66KKD	    KKY   (++D$7776{{{.. (('$3"/*?%50K&7 ) 
 
T #

$    $? (   --c22  5@$.BRBR4S+D1LLS
 #A    444& #

$    $#%   !MtTTTttt" #

$    $#!    --c22  5@$.BRBR4S+D1LLS
 #A    444 #

$    $#   !MtTTTttt#

$    $#!  #

$    $s   $3D  C//
C<;C< J!AGJ$ E1 1
E?>E?GJ$ &F< <
G
	G
J!AJ!J$ (H> >
IIJ,J$ 3J	 	
JJJ!!J$ $K(J=<K=
K
K	K

K$'list[tuple[str, Optional[object]]]'c                    	                      d          } n+# t          $ r                     j                  g} Y nw xY wg }| D ]>}|                    d          pj        }|                    | |          f           ?|S )a  Run one dispatch_once per board. Returns (slug, result) pairs.

            Enumerating boards on every tick keeps the dispatcher honest
            when users create a new board mid-run: no restart required,
            the next tick picks it up automatically.
            Fr   r   )r   r   r   r   r   r   )r   outbr   r   r  s       r    
_tick_oncezIGatewayKanbanWatchersMixin._kanban_dispatcher_watcher.<locals>._tick_once  s    F%@@ F F F11#2CDDEF8:C ? ?uuV}}9(9

D"6"6t"<"<=>>>>Js    %AAc                 f                                    } 	                     d          }n+# t          $ r                     j                  g}Y nw xY w|D ]T}|                    d          pj        }d}	                     |          }                    |          r0	 |*	 |                                  dS # t          $ r Y  dS w xY w dS | rE	                    |          r0	 |*	 |                                  dS # t          $ r Y  dS w xY w dS n8# t          $ r+ Y |&	 |                                 # t          $ r Y w xY ww xY w|(	 |                                 # t          $ r Y #w xY w)# |&	 |                                 w # t          $ r Y w w xY ww xY wdS )a~  Cheap probe: is there at least one ready+assigned+unclaimed
            task on ANY board whose assignee maps to a real Hermes profile
            (i.e. one the dispatcher would actually spawn for)?

            Tasks assigned to control-plane lanes (e.g. ``orion-cc``,
            ``orion-research``) are pulled by terminals via
            ``claim_task`` directly and never spawnable, so a queue full
            of those is "correctly idle", not "stuck". Filtering them out
            here keeps the stuck-warn fire only on real failures (broken
            PATH, missing venv, credential loss for a real Hermes profile).
            Fr   r   Nr   T)
review_dispatch_enabledr   r   r   r   r   r   has_spawnable_readyr8   has_spawnable_review)_review_prober   r  r   r   r   s        r    _ready_nonemptyzNGatewayKanbanWatchersMixin._kanban_dispatcher_watcher.<locals>._ready_nonempty  ss   $  7799MF%@@ F F F11#2CDDEF ! !uuV}}9(9!;;T;22D..t44 $# '! JJLLLLLL( ! ! ! DDD! ('' % $)A)A$)G)G $# '! JJLLLLLL( ! ! ! DDD! ('' !   '! JJLLLL( ! ! ! D! ( '! JJLLLL( ! ! ! D! (t'! JJLLLL( ! ! ! D! (
 5s   . %AA=+D ,C
CCD 3D


DDF 
E*F.E
EEEFE11
E?>E?F.FF.
F*	'F.)F*	*F.tuple[bool, int]c                 "    t                     S )z=Re-resolve (enabled, per_tick) from current config each tick.)r!   )_load_configs   r    _read_auto_decompose_settingsz\GatewayKanbanWatchersMixin._kanban_dispatcher_watcher.<locals>._read_auto_decompose_settings<  s    3LAAAr]   r   r   c           
        	 ddl m} n3# t          $ r&}t                              d|           Y d}~dS d}~ww xY w	                     d          }n+# t          $ r                     j                  g}Y nw xY wd}d}|D ]}|                    d          pj        }|| k    r nt          j
                            d          }	 |t          j
        d<   	 |                                }	n5# t          $ r(}t                              d	||           g }	Y d}~nd}~ww xY w|	D ]}
|| k    r n|d
z  }	 |                    |
d          }n+# t          $ r t                              d|
           Y Ow xY w|j        r`|d
z  }|j        r7|j        r0t                              d||
t'          |j                             t                              d||
           t                              d||
|j                   |"t          j
                            dd           |t          j
        d<   # |!t          j
                            dd           n|t          j
        d<   w xY w|S )zRun the auto-decomposer for up to N triage tasks across all
            boards. Returns the number of triage tasks that were
            successfully decomposed or specified this tick.
            r   )kanban_decomposez3kanban auto-decompose: import failed (%s); skippingNFr   r   HERMES_KANBAN_BOARDz>kanban auto-decompose: list_triage_ids failed on board %s (%s)r   zauto-decomposer)authorz3kanban auto-decompose: decompose_task crashed on %su.   kanban auto-decompose [%s]: %s → %d childrenu:   kanban auto-decompose [%s]: %s → single task (no fanout)z*kanban auto-decompose [%s]: %s skipped: %s)r   r  r   r'   r   r   r   r   r   rC  environlist_triage_idsrO   decompose_taskr  okfanout	child_idsr   r   r   r
  )r   _decomprU   r   	attempted	successesr  r   prev_env
triage_idstidoutcomer   s               r    _auto_decompose_tickzSGatewayKanbanWatchersMixin._kanban_dispatcher_watcher.<locals>._auto_decompose_tick@  s>   
BBBBBBB   I3   qqqqq	
F%@@ F F F11#2CDDEFII 8E 8EuuV}}9(9 777E
 :>>*?@@/E8<BJ45(%,%<%<%>%>

$ ( ( (\ #   &(





(  *    $(???!E!Q		%&-&<&< #,= '= ' 'GG  ) % % %",, U #   %H% #: %NI&~ 	"'2C 	" &$T$(#s73D/E/E!" !" !" !"
 !'$`$(#!" !" !" !" #LL L $c7>   
  '
'<dCCCC<D
#899  '
'<dCCCC<D
#89DDDDs|   
 
:5:A %A=<A=H<C32H<3
D%=D H< D%%H<:EH<%E:7H<9E::BH<<4I0z7kanban dispatcher: embedded in gateway (interval=%.1fs)z6kanban dispatcher: reaped %d zombie worker(s), pids=%sz'kanban dispatcher: zombie reaper failedFspawnedzckanban dispatcher [%s]: spawned=%d reclaimed=%d crashed=%d timed_out=%d promoted=%d auto_blocked=%d__len__zkanban dispatcher stuck: ready queue non-empty for %d consecutive ticks but 0 workers spawned. Check profile health (venv, PATH, credentials) and `hermes kanban list --status ready`.zkanban dispatcher: cancelledz+kanban dispatcher: unexpected watcher errorru   )r   r6   r   r  )rU   r   r   r   )r   r6   r   r  )r   r  r   r   )r   r  )r   r   r   r   )/r   r
   r   r'   r   rC  r  r   r|   r   r   r   r   r   rf   rZ   kanban_homer;   rb   r   r   r  r   resolve_max_in_progressDEFAULT_FAILURE_LIMITr   r   r   r   r  reap_worker_zombiesr   r  r(   rM   r  	reclaimedhasattrrj   rk   promotedauto_blockedr   CancelledErrorrO   r_   min)-r[   env_overrider   rU   
kanban_cfg
_lock_path_lock_handle_lock_statera   raw_max_in_progresseffective_max_in_progressraw_failure_limit	raw_staleraw_per_profileHEALTH_WINDOW	bad_tickslast_warn_atr  r  r  r  pidsready_pending_ad_enabled_ad_per_tickresultsany_spawnedr   resnowsleptr  r  r  r   r  r  r  r  r  r  r  r  r  r  s-                                  @@@@@@@@@@@@@@r    _kanban_dispatcher_watcherz5GatewayKanbanWatchersMixin._kanban_dispatcher_watcher  s     ,	EEEEEEE 	 	 	NNSTTTFF	 z~~&I2NNTTVV\\^^666KK_```F	,..CC 	 	 	NNQSVWWWFFFFF	 /9d.C.CKSWWXr***
~~3T:: 	KKY   F	3333333 	 	 	NN]^^^FF	 /3+__&&14FF
$;J$G$G!k+%%KK=>H   F&  2>D/KKSU_````NN+,6  
	Z^^,GLLRPRSSHHI& 	 	 	NN[:;;   HHH	 x%% NN;55	 KK99EEE )nn->EE*Z"%&9":": #Q&&NN[+   '+OOKK GYYYY z* ' ' 'T'   #''( %($?$?$P$P!"'@'LKKJ *	   4&NN?C<UVV	6 122MM:& 	6 	6 	6NNV!)  
  5MMM	6 1NNY!)  
  5M NN#CQGG		&$'	Q$7$7!!:& 	& 	& 	&NN,  
 %&!!!	& !0CT!J!JKK 'NN+=>>D"KKMMUQU 	KK.    %..)FMM&*#&.1/.B.B+ /22NNg'   37//KKK3    z* 3 3 3`#   /3+++3, mA
 	 -0)  	 
	> 
	> 
	> 
	> 
	> 
	>
	 
	 
	 
	 
	 
	W	 W	 W	 W	 W	 W	 W	 W	 W	 W	 W	 W	 W	 W	 W	 W	 W	r	 	 	 	 	 	 	"(	 (	 (	 (	 (	 (	r	B 	B 	B 	B 	B 	BK	 K	 K	 K	 K	 K	Z 	Ex	
 	
 	
 m K	L %.s/FGGGGGGGG KKPD		  
  L L L  !JKKKKKL6P 011 !&$)M !II
 1N0M0O0O-K" T%/0DlSSSSSSSSS$+$5j$A$AAAAAAAG"'K&-m  	c?wsIt/L/L?*.K #KK!V $ #CK 0 0 #4;CK4S4S ZCK 0 0 0YZ6=cmY6W6W ^CM 2 2 2]^ #9@AQS\9]9] dC$4 5 5 5cd
 
 
 +2*;O*L*L$L$L$L$L$L$LM$ &[ &!Q		$%	--dikk**C\)S00C &   (+)   ;<<<44666 P P P  !NOOOOOP
 E(""t}"mCX-=$>$>????????? (""t}"S m K	Z 	,,.....s    $A A)
B4 4
C$>CC$E $E65E6%H: :AI>=I>L. ..MM4O 9P ?P Q .R
RU# #.VVA
Y   $ZZF#`/ /A!bbr  r   )r`   )ra   rb   r   r<   r{   )rA   r   r   r   r   rB   r   r<   )rA   r   r   rB   r   r<   )
rA   r   r<  r   r   r   r   rB   r   r<   )rI   r6   r   r   r   r?  r   r<   )__name__
__module____qualname____doc__r\   r_   r6  r  r  r  r	  r  r   r]   r    rX   rX      s        IIQ Q Q Q( ( ( (A' A' A' A' A'H >B    ,    &  $    .k k k kZ/ / / / / /r]   rX   )r
   r   r   r   r  )r   r)   r   )r@   r   rA   r   r   rB   )r  
__future__r   r   loggingrC  r  r   re  r   typingr   r   r   
agent.i18nr	   	getLoggerr'   r!   r(   r;   r?   rV   rX   r   r]   r    <module>r     sI    # " " " " "   				         * * * * * * * * * *       
	=	)	)   @. . . . " " " "J   # # # #Lw/ w/ w/ w/ w/ w/ w/ w/ w/ w/r]   