
    (Gj                        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 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/rurouni/.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+'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$   ImportErrorr   parentmkdiropenstrOSErrorclose)	lock_pathr$   handles      r    _acquire_singleton_lockr5   <   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)r+   r8   r   r2   )r4   r8   s     r    _release_singleton_lockr9   a   s    ~5555556""""      s    
%%? 
AAc                  N    e Zd Z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.      @intervalfloatr   r6   c                   789: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} 	 ddlm9 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   j        	rs	 79: fd}
t+          j        |
           d{V }|D ]}|d         }|d         }|                    d          }|d         pd	                                }	  ||          }n8# t2          $ r+ t+          j         j        ||d         |           d{V  Y w xY w|                    d          pd}                     ||pd          }|at                              d||d                    t+          j         j        ||d         |                    dd          |           d{V  |r|j        n|d         dd          }|rd!| d"nd}|d#         D ]}|j        }|r|j         r|j         nd}|rd$| d%nd}|d&k    rd}d}|j!        r4|j!                            d'          rtE          |j!        d'                   }|rH|                                #                                }|r|d         dd(         n	|dd(         }d)| }nZ|rX|j$        rQ|j$                                        #                                }|r|d         dd*         n|j$        dd*         }d)| }d+| | d,|d          d-| | }n|d.k    r]d} |j!        r?|j!                            d/          r%d0tE          |j!        d/                   dd*          } d1| | d,|d          d2|  }n+|d3k    r\d}!|j!        r?|j!                            d4          r%d)tE          |j!        d4                   dd(          }!d5| | d,|d          d6|! }n|d7k    rd5| | d,|d          d8}n|d9k    rRd}"|j!        r4|j!                            d:          rtK          |j!        d:                   }"d;| | d,|d          d<|" d=}nY|d>k    rQd}#|j!        r4|j!                            d>          rtE          |j!        d>                   }#d?| | d,|d          d@|# }ni }$|                    dA          r|dA         |$dA<   |d         |d         |dB         |                    dA          pdf}%	 |&                    |dB         ||$C           d{V  t                              dD||d         ||dB         |           |d&k    rp	  '                    ||dB         |$t#          |dEd          |F           d{V  n9# t          $ r,}&t                              dG|d         |&           Y d}&~&nd}&~&ww xY w|	(                    |%d           # t          $ r}|	                    |%d          dHz   }'|'|	|%<   t                              dI|d         ||'||           |'|k    r[t                              dJ|d         ||'           t+          j         j)        ||           d{V  |	(                    |%d           n=t+          j         j        ||d         |                    dd          |           d{V  Y d}~ nd}~ww xY wt+          j         j        ||d         |           d{V  |o|j*        dKv }(dL88fdM|d#         D             })|)r.	 t#          |dNd          pd}*|*r|r|j        n|d         dd          }+|r|j         nd},g }-d&|)v r"|-+                    tY          dO                     d3|)v r"|-+                    tY          dP                     d7|)v r"|-+                    tY          dQ                     d9|)v r"|-+                    tY          dR                     d.|)v r"|-+                    tY          dS                     tY          dT          -                    |-          ptY          dU          }.tY          dV|d         |.|+|,|W          }/ddXl.m/}0 ddYl0m1}1m2}2  |0||dB         dZ|                    dA          pd|                    d[          |pd\          }3 |1|/|2j3        |3d]          }4|4                    |4           d{V  t          
                    d^|d         ||dB         |pd_|)           n;# t          $ r.}5t                              d`|d         |5da           Y d}5~5nd}5~5ww xY w|(r!t+          j         j)        ||           d{V  n2# t          $ r%}t                              db|           Y d}~nd}~ww xY wtk          tK          tm          dH|                              D ]&}6 j        s dS t+          j        dH           d{V  ' j        	qdS dS )cu  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``). Sends one
        message per new event to ``(platform, chat_id, thread_id)``,
        then advances the cursor. When a task reaches a terminal state
        (``completed`` / ``archived``), the subscription is removed.

        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. Subscriptions live inside each board's own DB and cannot
        cross boards, so delivery semantics are unchanged — this is
        purely a fan-out of the single-DB poll.
        r   r
   z4kanban notifier: config loader unavailable; disabledN!HERMES_KANBAN_DISPATCH_IN_GATEWAY >   0noofffalsezCkanban notifier: disabled via HERMES_KANBAN_DISPATCH_IN_GATEWAY envz2kanban notifier: cannot load config (%s); disabledr   dispatch_in_gatewayTzEkanban notifier: disabled via config kanban.dispatch_in_gateway=false)Platform	kanban_dbz<kanban notifier: kanban_db not importable; notifier disabled)	completedblockedgave_upcrashed	timed_outstatusarchived	unblockedr   _kanban_sub_fail_counts_kanban_notifier_profile   c                    g } d j                                         D             }|st                              d           | S 	                     d          }n+# t
          $ r                     j                  g}Y nw xY wt                      }|D ]4}|	                    d          pj        }|	                    d          }	 |r@t          t          |                                                                                    n3t                              |                                                    }n# t
          $ r d| }Y nw xY w||v rt                              d||           |                    |           	                     |	          }n4# t
          $ r'}	t                              d
||	           Y d }	~	;d }	~	ww xY w	                     |          }
|
st                              d|           |
D ]}|	                    d          pd }|r]|k    rWt%          di           	                    |          }|s1t                              d|	                    d          |           y|	                    d          pd                                }||vr2t                              d|	                    d          |pd           ؉                    ||d         |d         |d         |	                    d          pd          \  }}}|s"                    ||d                   }t                              dt-          |          |d         |||           |                     ||||||d           	 |                                 # |                                 w xY w| S )Nc           	     n    h | ]2}t          |d t          |                                                    3S )value)getattrr0   lower).0platforms     r    	<setcomp>zXGatewayKanbanWatchersMixin._kanban_notifier_watcher.<locals>._collect.<locals>.<setcomp>   sF     ( ( ($  '3x==AAGGII( ( (    z5kanban notifier: no connected adapters; skipping tickFinclude_archivedslugdb_pathzslug:z;kanban notifier: skipping duplicate board slug %s for DB %sboardz)kanban notifier: cannot open board %s: %sz.kanban notifier: board %s has no subscriptionsnotifier_profile_profile_adapterszlkanban notifier: subscription for %s owned by profile %s; current profile %s has no adapter for it, skippingtask_idr\   rB   zIkanban notifier: subscription for %s on %s skipped; adapter not connectedz	<missing>chat_id	thread_id)rg   r\   rh   ri   kindsuF   kanban notifier: claimed %d event(s) for %s on board %s cursor %s→%s)sub
old_cursorcursoreventstaskrd   )adapterskeysloggerdebuglist_boardsr   read_board_metadataDEFAULT_BOARDsetr   r0   r   
expanduserresolvekanban_db_pathaddconnectlist_notify_subsrY   rZ   claim_unseen_events_for_subget_tasklenappendr2   )
deliveriesactive_platformsboardsseen_db_paths
board_metara   rb   resolved_db_pathconnexcsubsrk   owner_profile_owner_adaptersr\   rl   rm   rn   ro   TERMINAL_KINDS_kbre   selfs                      r    _collectzEGatewayKanbanWatchersMixin._kanban_notifier_watcher.<locals>._collect   s   -/J( ((,(:(:(<(<( ( ($ , *%\]]]))N!$%!H!H$ N N N"%"9"9#:K"L"L!MN.1eeM&, L) L)
)~~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   %%))*:;;;%#&;;T;#:#:DD( % % %"LL)TVZ\_```$HHHH%9) $'#7#7#=#=D#' e &-]_c d d d'+ '# '#038J0K0K0St#0 !1]FV5V5V6=dDWY[6\6\6`6`an6o6oO+: %1(. -[,/GGI,>,>O_)* )* )* )1,/GGJ,?,?,E2+L+L+N+N#+3C#C#C$*LL(s(+	(:(:H<S%& %& %& %-=@=\=\$(,/	N-0_,/	N.1ggk.B.B.Hb*8 >] >" >" :
FF (. !-$,'*||D#i.'I'I &$l$'KKYzSY!" !" !" !+ 1 1+.2<.4.4,0-13" 3" !# !# !# !#A'#R !JJLLLLDJJLLLL%%sJ   A %BBA6EEEF''
G1GGGN55Ork   ro   rd   r\   rm   re   zPkanban notifier: adapter %s disconnected before delivery for %s; rewinding claimrg   rl   x   [z] rn   @ rK   summary   
   u   ✔ zKanban u
    done — rL   reasonz: u   ⏸ z blockedrM   erroru   ✖ z& gave up after repeated spawn failuresrN   z1 worker crashed (pid gone); dispatcher will retryrO   limit_secondsu   ⏱ z timed out (max_runtime=zs); will retryrP   u   🔄 u    → ri   rh   )metadataz?kanban notifier: delivered %s event for %s to %s/%s on board %spayload)adapterrh   r   event_payloadro   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 failures>   donerQ   )rK   rM   rN   rO   rL   c                0    h | ]}|j         v |j         S  )kind)r[   ev_WAKE_KINDSs     r    r]   zFGatewayKanbanWatchersMixin._kanban_notifier_watcher.<locals>.<setcomp>  s(    &_&_&_2S^H^H^rwH^H^H^r^   
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)rg   rP   titleassigneerd   )SessionSource)MessageEventMessageTypegroupuser_id)r\   rh   	chat_typeri   r   profile)textmessage_typesourceinternalz@kanban notifier: woke agent for %s on %s/%s profile=%s events=%sdefaultz3kanban notifier: wakeup injection failed for %s: %s)exc_infozkanban notifier tick failed: %s)7hermes_cli.configr
   r   rr   warningosenvironr   striprZ   infor   r   gateway.configrH   
hermes_clirJ   rY   rS   _active_profile_namerT   asynciosleep_running	to_threadr   _kanban_advance_authorization_adapterrs   _kanban_rewindr   r   r   r   r0   
splitlinesresultr   send_deliver_kanban_artifactspop_kanban_unsubrP   r   r	   joingateway.sessionr   gateway.platforms.baser   r   TEXThandle_messagerangemax);r   r=   _load_configenv_overrider   r   
kanban_cfg	_PlatformMAX_SEND_FAILURESsub_fail_countsr   r   drk   ro   
board_slugplatform_strplatsub_profiler   r   	board_tagr   r   whotaghandoffpayload_summarylineshrmsgr   errlimit
new_statusr   sub_keyart_excfailstask_terminal_wake_kinds_session_key_title	_assignee_parts_status_synthr   r   r   _source_synth_event_wk_err_r   r   r   re   s;   `                                                      @@@@r    _kanban_notifier_watcherz3GatewayKanbanWatchersMixin._kanban_notifier_watchers   s     .	EEEEEEE 	 	 	NNQRRRFF	 z~~&I2NNTTVV\\^^666KK]^^^F	,..CC 	 	 	NNOQTUUUFFFFF	 /9d.C.CKSWWXr***
~~3T:: 	KKW   F888888	3333333 	 	 	NNYZZZFF	 x" ,3+R-
 -
 (7$"4)CTJJ 	=#88::,<D) mAm y	'sGa& a& a& a& a& a& a& a&F $+#4X#>#>>>>>>>
# L L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k d d!w 15PPt}}D,/7j#jjjjR;.. ')G.2O!z MbjnnY.G.G M25bj6K2L2L. 3(7(=(=(?(?(J(J(L(L6;$VE!HTcTNNQURUQUAV*2q((!% 3$+ 3(,(9(9(;(;(F(F(H(H6;$RE!HTcTNNTcTAR*2q((!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 %3577;// E474DH[1	NC
O	NCGGK,@,@,FB#=""),, #Ih #/ # #        #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"B &/ 0#q{J         )-(T@T1T&a&_&_&_&_8&_&_&_& E"D"/6t\4/P/P/VTV#/ 7!&<@.Tdjjc)nVZWZVZ-[FAE0M2I-/F'2k'A'A6==QRSrQsQsCtCtCt'0K'?'?qQnOoOoApApAp'0K'?'?qQnOoOoApApAp'2k'A'A6==QRSrQsQsCtCtCt'0K'?'?qQnOoOoApApAp./0S.T.T.Y.YZ`.a.a  /Mef  hL  fM  fMG-.(E03I/6.41:.8.& .& .&F %N$M$M$M$M$M$`$`$`$`$`$`$`$`$ /<m1503I2925''+2F2F2N$03	0B0B0;0Ct/& /& /&G 4@<-35@5E/615	4& 4& 4&L +2*@*@*N*N$N$N$N$N$N$N$N$*KK(j(+Ic)nVaVnenp{%& %& %& $- 
" 
" 
" !'$Y$'	NGd !/ !" !" !" !" !" !" !" !"
" ) ")"3 $ 2C# #       ULZ  G G G@#FFFFFFFFG 3s1h//0011 ' '} FFmA&&&&&&&&&&s m y	' y	' y	' y	' y	's    $88 
B+ +
C5CCE $E32E3#A(k Ik 2J
k JOk A\.'5[\.
\'"\	\.\\.,k .
`%8C!` k  `%%Ak 5G1i'&k '
j1$jk j(k 
k7k22k7Nrk   r   rm   r   rd   Optional[str]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   rI   rc   rg   r\   rh   ri   rB   )rg   r\   rh   ri   
new_cursorN)r   rJ   r|   advance_notify_cursorr   r2   )r   rk   rm   rd   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   rI   rc   rg   r\   rh   ri   rB   )rg   r\   rh   ri   )r   rJ   r|   remove_notify_subr   r2   )r   rk   rd   r   r   s        r    r   z(GatewayKanbanWatchersMixin._kanban_unsubV  s    //////{{{''		!!IZI''+..4" "    JJLLLLLDJJLLLLs   AA5 5Bclaimed_cursorrl   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   rI   rc   rg   r\   rh   ri   rB   )rg   r\   rh   ri   r   rl   N)r   rJ   r|   rewind_notify_cursorr   r2   )r   rk   r   rl   rd   r   r   s          r    r   z)GatewayKanbanWatchersMixin._kanban_rewindd  s     	0/////{{{''	$$IZI''+..4"-% %    JJLLLLLDJJLLLLs   AA7 7Brh   r0   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   pathr0   r   r6   c                    | sd S t           j                            |           }|v rd S t           j                            |          sd S                     |                               |           d S N)r   r  rx   isfiler{   r   )r  expanded
candidatesseens     r    _addzBGatewayKanbanWatchersMixin._deliver_kanban_artifacts.<locals>._add  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   suffixrZ   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   r  r  s     r    r"  zHGatewayKanbanWatchersMixin._deliver_kanban_artifacts.<locals>.<listcomp>  s9    [[[Qa0E0E0G0G{0Z0Zq0Z0Z0Zr^   c                0    g | ]}d  |           dfS )zfile://rB   r   )r[   r  _quotes     r    r"  zHGatewayKanbanWatchersMixin._deliver_kanban_artifacts.<locals>.<listcomp>  s/    JJJ/FF1II//4JJJr^   )rh   imagesr   z.kanban notifier: image batch upload failed: %s)rh   
video_pathr   )rh   	file_pathr   z0kanban notifier: artifact upload (%s) failed: %s)r  r0   r   r6   )pathlibr   rw   r   r   r   listtupler0   extract_local_filesrY   r   r   r  filter_local_delivery_pathsurllib.parser  send_multiple_imagesr   rr   r   r  rZ   
send_videosend_document)r   r   rh   r   r   ro   r  rawitemr   pathsr   r  result_textr  _VIDEO_EXTSimage_pathsother_pathsbatchr   r  extr   r!  r%  r
  r  s                         @@@@@r    r   z4GatewayKanbanWatchersMixin._deliver_kanban_artifacts{  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                F   !"#$%&'()*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'nRt          
                    d '            n4# t(          t&          f$ r  t                              d!|	           d'Y nw xY w|                    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|                    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t1          j        d-           d{V  d.}d}d}d/i %dN!fd4dO!fd8 dP !$%&'()*fd:#dQ!#fd<}dR!fd=}dS"fd?}dT!fdB}t          
                    dC|           | j        r	 t1          j        !j                   d{V }|r)t          
                    dDt;          |          |           n*# t          $ r t                              dE           Y nw xY w	  |            \  }}|rt1          j        ||           d{V  t1          j        |           d{V }dF}|pg D ]\  }}|t?          |dGd          rd}t          
                    dH|t;          |j                   |j!        tE          |j#        dI          rt;          |j#                  ndtE          |j$        dI          rt;          |j$                  nd|j%        tE          |j&        dI          rt;          |j&                  nd           t1          j        |           d{V }|r|s|dz  }nd}||k    rFt-          tO          j'                              }||z
  d/k    rt                              dJ|           |}nn# t0          j(        $ r7 t          )                    dK           tU          | j                   d| _         t          $ r t                              dL           Y nw xY wdM}||k     rD| j        r=t1          j        tW          d||z
                       d{V  |dz  }||k     r| j        =| j        tU          | j                   d| _        dS )Uu  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   r@   z6kanban dispatcher: config loader unavailable; disabledNrA   rB   >   rC   rD   rE   rF   zEkanban dispatcher: disabled via HERMES_KANBAN_DISPATCH_IN_GATEWAY envz4kanban dispatcher: cannot load config (%s); disabledr   rG   TzGkanban dispatcher: disabled via config kanban.dispatch_in_gateway=falserI   z@kanban dispatcher: kanban_db not importable; dispatcher disabledz.dispatcher.lockr)   zjkanban dispatcher: another gateway already holds the dispatcher lock (%s); this gateway will NOT dispatch.r*   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=max_in_progressr   zAkanban dispatcher: kanban.max_in_progress=%r is below 1; ignoringz#kanban dispatcher: max_in_progress=z>kanban dispatcher: invalid kanban.max_in_progress=%r; ignoring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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; ignoringrU      i,  ra   r0   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  )	rz   r0   rx   ry   r   statr1   st_mtime_nsst_size)ra   r  resolvedrG  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r   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)rY   r   sqlite3DatabaseErrorr0   rZ   )r   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 dispatchrc   )rd   r>  r?  r@  stale_timeout_secondsrB  rC  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   time	monotonicrr   r   r   r|   dispatch_oncer2   r   rN  rO  r   	exception)ra   r   fingerprintdisabled_entrydisabled_fingerprintdisabled_atager   !CORRUPT_BOARD_RETRY_AFTER_SECONDSrK  rQ  r   rB  disabled_corrupt_boardsr@  r?  rC  r>  rT  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5{{{.. (('$3"/*?%50K ) 	 	R #

$    $? (   --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   $2C? C..
C;:C;?J AGJ# E0 0
E>=E>GJ# %F; ;
G	G	J AJ J# 'H= =
I
IJ+J# 2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_   ra   )rt   r   ru   rv   r   r   )r   outbra   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                 <   	                      d          } n+# t          $ r                     j                  g} Y nw xY w| D ]S}|                    d          pj        }d}	                     |          }                    |          r0	 |*	 |                                  dS # t          $ r Y  dS w xY w dS                     |          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_   ra   Nrc   T)	rt   r   ru   rv   r   r|   has_spawnable_readyr2   has_spawnable_review)r   rd  ra   r   r   s       r    _ready_nonemptyzNGatewayKanbanWatchersMixin._kanban_dispatcher_watcher.<locals>._ready_nonempty8  s`   F%@@ F F F11#2CDDEF ! !uuV}}9(9!;;T;22D..t44 $# '! JJLLLLLL( ! ! ! DDD! ('' //55 $# '! JJLLLLLL( ! ! ! DDD! (''$    '! JJLLLL( ! ! ! D! ($
 '! JJLLLL( ! ! ! D! (t'! JJLLLL( ! ! ! D! (
 5s    %AA)+D
B//
B>=B>D
C44
DD	E/

D?E/D--
D:9D:>D??E/E
E*)E*/F3FF
F	FF	Ftuple[bool, int]c                 "    t                     S )z=Re-resolve (enabled, per_tick) from current config each tick.)r!   )r   s   r    _read_auto_decompose_settingsz\GatewayKanbanWatchersMixin._kanban_dispatcher_watcher.<locals>._read_auto_decompose_settingsj  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_   ra   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   rn  r   rr   r   rt   ru   rv   r   r   r   list_triage_idsrs   decompose_taskrX  okfanout	child_idsr   r   r   r   )r   _decompr   r   	attempted	successesrd  ra   prev_env
triage_idstidoutcomer   s               r    _auto_decompose_tickzSGatewayKanbanWatchersMixin._kanban_dispatcher_watcher.<locals>._auto_decompose_tickn  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 errorg        )ra   r0   r   rE  )r   r   r   r   )ra   r0   r   rR  )r   ra  )r   r   )r   rj  )r   r   r   r   ),r   r
   r   rr   r   r   r   r   r   rZ   r   r   r   r   rJ   _kanban_dispatcher_lock_handlekanban_homer5   r>   r   r   r   r   DEFAULT_FAILURE_LIMITr   r   r   r   reap_worker_zombiesr   rX  rY   r~  	reclaimedhasattrrN   rO   promotedauto_blockedrU  CancelledErrorrs   r9   min)+r   r   r   r   r   
_lock_path_lock_handle_lock_stater=   raw_max_in_progressraw_failure_limit	raw_staleraw_per_profileHEALTH_WINDOW	bad_tickslast_warn_atre  ri  rl  r}  pids_ad_enabled_ad_per_tickresultsany_spawnedra   resready_pendingnowsleptr^  rK  rQ  r   r   r`  rB  r_  r@  r?  rC  r>  rT  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C	CCDDD )nn->EE*Y"%&9":": #Q&&NN[+   '+OOKK Wo W WXXXX z* ' ' 'T'   #''  '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,  
 %&!!!	& '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)  	 
	> 
	> 
	> 
	> 
	> 
	>
	 
	 
	 
	 
	 
	V	 V	 V	 V	 V	 V	 V	 V	 V	 V	 V	 V	 V	 V	 V	 V	p	 	 	 	 	 	 	"!	 !	 !	 !	 !	 !	d	B 	B 	B 	B 	B 	BK	 K	 K	 K	 K	 K	Z 	Ex	
 	
 	
 m E	L %.s/FGGGGGGGG KKPD		  
  L L L  !JKKKKKL0P -J,I,K,K)\ P!+,@,OOOOOOOOO ' 1* = =======#")-R  ID#73	4+H+H&* R ,,M07Y0O0OVC,,,UV29#-2S2SZC...YZL5<S=My5Y5Y`C 0111_`
 
 
 '.&7&H&H H H H H H H  " "NII !I--dikk**C\)S00C &   (+)   ;<<<'(KLLL6:3 P P P  !NOOOOOP
 E(""t}"mCX-=$>$>????????? (""t}"G m E	N 	  CDDD.2+++s    $A A (
B3 3
C#=CC#E $E54E5%H9 9AI=<I= L1 1.M"!M"N 9OOP' '.QQ>T .T>=T>>A
X	 	$X0/X04F_ A(`/.`/)r<   )r=   r>   r   r6   r  )rk   r   rm   r   rd   r   r   r6   )rk   r   rd   r   r   r6   )
rk   r   r   r   rl   r   rd   r   r   r6   )rh   r0   r   r   r   r  r   r6   r   r6   )
__name__
__module____qualname____doc__r   r   r   r   r   r  r   r^   r    r;   r;   p   s        IIK' K' K' K' K'\ >B    ,    &  $    .k k k kZ^3 ^3 ^3 ^3 ^3 ^3r^   r;   )r
   r   r   r   )r   r"   r  )r  
__future__r   r   loggingr   rN  rU  r)  r   typingr   r   r   
agent.i18nr	   	getLoggerrr   r!   r5   r9   r;   r   r^   r    <module>r     s     # " " " " "   				         * * * * * * * * * *       
	=	)	)   @" " " "J   V3 V3 V3 V3 V3 V3 V3 V3 V3 V3r^   