
    oq'j3                        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  ej        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Optionalzgateway.runc                  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floatreturnNonec                   '()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( n+# t          $ r t                              d           Y dS w xY wd'd}t#           di           }	|	 _        t#           dd          ))s                                 )) _        t+          j        d           d{V   j        r	 '() 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 j                            |          }|at                              d||d                    t+          j         j        ||d         |                    dd          |           d{V  |r|j        n|d         dd         }|d          D ]v}|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    rZd}|j!        r?|j!                            d,          r%d-tE          |j!        d,                   dd'          }d.| d)|d          d/| }n|d0k    rZd}|j!        r?|j!                            d1          r%d&tE          |j!        d1                   dd%          }d2| d)|d          d3| }nn|d4k    rd2| d)|d          d5}nX|d6k    rPd} |j!        r4|j!                            d7          rtK          |j!        d7                   } d8| d)|d          d9|  d:}nRi }!|                    d;          r|d;         |!d;<   |d         |d         |d<         |                    d;          pdf}"	 |&                    |d<         ||!=           d{V  t                              d>||d         ||d<         |           |d#k    rp	  '                    ||d<         |!t#          |d?d          |@           d{V  n9# t          $ r,}#t                              dA|d         |#           Y d}#~#nd}#~#ww xY w|	(                    |"d           # t          $ r}|	                    |"d          dBz   }$|$|	|"<   t                              dC|d         ||$||           |$|k    r[t                              dD|d         ||$           t+          j         j)        ||           d{V  |	(                    |"d           n=t+          j         j        ||d         |                    dd          |           d{V  Y d}~ n^d}~ww xY wt+          j         j        ||d         |           d{V  |o|j*        dEv }%|%r!t+          j         j)        ||           d{V  n2# t          $ r%}t                              dF|           Y d}~nd}~ww xY wtW          tK          tY          dB|                              D ]&}& j        s dS t+          j        dB           d{V  ' j        dS dS )Gu  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   load_configz4kanban 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); disabledkanban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   _kanban_sub_fail_counts_kanban_notifier_profile   c                 R   g } d j                                         D             }|st                              d           | S 	                     d          }n+# t
          $ r                     j                  g}Y nw xY wt                      }|D ]}|	                    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 ]e}|	                    d          pd }|r7|k    r1t                              d|	                    d          |           S|	                    d          pd                                }||vr2t                              d|	                    d          |pd                               ||d         |d         |d         |	                    d          pd          \  }}}|s                    ||d                   }t                              dt+          |          |d         |||           |                     ||||||d           g	 |                                 # |                                 w xY w| S )Nc           	     n    h | ]2}t          |d t          |                                                    3S )value)getattrstrlower).0platforms     </home/ubuntu/.hermes/hermes-agent/gateway/kanban_watchers.py	<setcomp>zXGatewayKanbanWatchersMixin._kanban_notifier_watcher.<locals>._collect.<locals>.<setcomp>q   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_profilezUkanban notifier: subscription for %s owned by profile %s; current profile %s skippingtask_idr-   r   zIkanban notifier: subscription for %s on %s skipped; adapter not connectedz	<missing>chat_id	thread_id)r8   r-   r9   r:   kindsuF   kanban notifier: claimed %d event(s) for %s on board %s cursor %s→%s)sub
old_cursorcursoreventstaskr6   )adapterskeysloggerdebuglist_boards	Exceptionread_board_metadataDEFAULT_BOARDsetgetr*   r   
expanduserresolvekanban_db_pathaddconnectlist_notify_subsr+   claim_unseen_events_for_subget_tasklenappendclose)
deliveriesactive_platformsboardsseen_db_paths
board_metar3   r4   resolved_db_pathconnexcsubsr<   owner_profiler-   r=   r>   r?   r@   TERMINAL_KINDS_kbr7   selfs                     r.   _collectzEGatewayKanbanWatchersMixin._kanban_notifier_watcher.<locals>._collecto   s_   -/J( ((,(:(:(<(<( ( ($ , *%\]]]))N!$%!H!H$ N N N"%"9"9#:K"L"L!MN.1eeM&, J) J)
)~~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%7) $'#7#7#=#=D#' e &-]_c d d d'+ %# %#038J0K0K0St#0 !-]FV5V5V$*LL((+	(:(:MK[%& %& %& %-,/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" !# !# !# !#=%#N !JJLLLLDJJLLLL%%sJ   A %BBA6EEEF''
G1GGFNN$r<   r@   r6   r-   r>   zPkanban notifier: adapter %s disconnected before delivery for %s; rewinding claimr8   r=   x   r?   @ r   summary   
   u   ✔ zKanban u
    done — r   reasonz: u   ⏸ z blockedr   erroru   ✖ z& gave up after repeated spawn failuresr    z1 worker crashed (pid gone); dispatcher will retryr!   limit_secondsu   ⏱ z timed out (max_runtime=zs); will retryr:   r9   )metadataz?kanban notifier: delivered %s event for %s to %s/%s on board %spayload)adapterr9   rn   event_payloadr@   z4kanban notifier: artifact delivery for %s failed: %s   z=kanban notifier: send failed for %s on %s (attempt %d/%d): %szRkanban notifier: dropping subscription %s on %s after %d consecutive send failures>   donearchivedzkanban notifier tick failed: %s)-hermes_cli.configr   rF   rC   warningosenvironrJ   stripr+   info
isinstancedictgateway.configr   
hermes_clir   r)   r#   _active_profile_namer$   asynciosleep_running	to_thread
ValueError_kanban_advancerA   rD   _kanban_rewindtitlekindassigneero   r*   
splitlinesresultintsend_deliver_kanban_artifactspop_kanban_unsubstatusrangemax)*rb   r   _load_configenv_overridecfgr]   
kanban_cfg	_PlatformMAX_SEND_FAILURESsub_fail_countsrc   rV   dr<   r@   
board_slugplatform_strplatrp   r   evr   whotaghandoffpayload_summarylineshrmsgrk   errlimitrn   sub_keyart_excfailstask_terminal_r`   ra   r7   s*   `                                      @@@r.   _kanban_notifier_watcherz3GatewayKanbanWatchersMixin._kanban_notifier_watcher   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	 U" ,3+R-
 -
 (7$"4)CTJJ 	=#88::,<D) mAm X	'RG_& _& _& _& _& _& _& _&B $+#4X#>#>>>>>>>
# m mAE(CV9D!"wJ$'
O$9r#@#@#B#BL!(y66% ! ! ! &/ 0#q{J         !! #m//55Gn(#i.   &/ /hKEE,22&         !+/CTZZS^TcTJEk P P!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((!9s !9 !93y> !9 !9(-!9/6!9 !9  C "Y..%'F!z PbjnnX.F.F P)Oc"*X2F.G.G.M)O)O"U"U"US^"U"UV"U"UCC!Y.."$C!z LbjnnW.E.E L&K3rz'/B+C+CDSD+I&K&K!Fs !F !F3y> !F !F@C!F !F  C "Y..!Es !E !E3y> !E !E !E  C "[00$%E!z Ibjnn_.M.M I(+BJ,G(H(H!Fs !F !F3y> !F !F05!F !F !F  C
 %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( ")"3 $ 2C# #       Wm\  G G G@#FFFFFFFFG 3s1h//0011 ' '} FFmA&&&&&&&&&&q m X	' X	' X	' X	' X	's    $77
B* *
C4CCE
 
$E21E2"A(_> I_> 2J	_> JM_> AZ,%5YZ,
Z%"ZZ,ZZ,*_> ,
^"6C!^_> ^""A_> >
`-`((`-Nr<   r|   r>   r   r6   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   r   r5   r8   r-   r9   r:   r   )r8   r-   r9   r:   
new_cursorN)r~   r   rO   advance_notify_cursorrJ   rU   )rb   r<   r>   r6   ra   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   r   r5   r8   r-   r9   r:   r   )r8   r-   r9   r:   )r~   r   rO   remove_notify_subrJ   rU   )rb   r<   r6   ra   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   r   r5   r8   r-   r9   r:   r   )r8   r-   r9   r:   r   r=   N)r~   r   rO   rewind_notify_cursorrJ   rU   )rb   r<   r   r=   r6   ra   r\   s          r.   r   z)GatewayKanbanWatchersMixin._kanban_rewind  s     	0/////{{{''	$$IZI''+..4"-% %    JJLLLLLDJJLLLLs   AA7 7Br9   r*   rn   rq   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   pathr*   r   r   c                    | sd S t           j                            |           }|v rd S t           j                            |          sd S                     |                               |           d S N)rw   r   rK   isfilerN   rT   )r   expanded
candidatesseens     r.   _addzBGatewayKanbanWatchersMixin._deliver_kanban_artifacts.<locals>._add  s|     w))$//H47>>(++ HHXh'''''r0   	artifactsrg   Nr   )BasePlatformAdapter>   .gif.jpg.png.jpeg.webp>   .3gp.avi.mkv.mov.mp4.webm)quotec                \    g | ](} |          j                                         v &|)S  suffixr+   r,   p_IMAGE_EXTS_Paths     r.   
<listcomp>zHGatewayKanbanWatchersMixin._deliver_kanban_artifacts.<locals>.<listcomp>  s9    WWWQa0E0E0G0G;0V0Vq0V0V0Vr0   c                \    g | ](} |          j                                         v&|)S r   r   r   s     r.   r   zHGatewayKanbanWatchersMixin._deliver_kanban_artifacts.<locals>.<listcomp>  s9    [[[Qa0E0E0G0G{0Z0Zq0Z0Z0Zr0   c                0    g | ]}d  |           dfS )zfile://r   r   )r,   r   _quotes     r.   r   zHGatewayKanbanWatchersMixin._deliver_kanban_artifacts.<locals>.<listcomp>  s/    JJJ/FF1II//4JJJr0   )r9   imagesrn   z.kanban notifier: image batch upload failed: %s)r9   
video_pathrn   )r9   	file_pathrn   z0kanban notifier: artifact upload (%s) failed: %s)r   r*   r   r   )pathlibr   rI   r{   r|   rJ   listtupler*   extract_local_filesr)   r   gateway.platforms.baser   filter_local_delivery_pathsurllib.parser   send_multiple_imagesrF   rC   rv   r   r+   
send_videosend_document)rb   rp   r9   rn   rq   r@   r   rawitemrg   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                Z   !"#$%&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	 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t+          j        d'           d{V  d(}d}d}d)i !dHfd.dIfd2dJ !"#$%&fd4dKfd6}dLfd7}t/          |                    d8d                    }	 t'          |                    d9d:          pd:          n# t"          t           f$ r d:Y nw xY wdk     rddMfd<}t          
                    d=|           | j        r	 t+          j        j                   d{V }|r)t          
                    d>t7          |          |           n*# t          $ r t                              d?           Y nw xY w	 |rt+          j        |           d{V  t+          j        |           d{V }d@}|pg D ]\  }}|t;          |dAd          rd}t          
                    dB|t7          |j                  |j        tA          |j!        dC          rt7          |j!                  ndtA          |j"        dC          rt7          |j"                  nd|j#        tA          |j$        dC          rt7          |j$                  nd           t+          j        |           d{V }|r|s|dz  }nd}||k    rFt'          tK          j%                              }||z
  d)k    rt                              dD|           |}nS# t*          j&        $ r t          '                    dE            t          $ r t                              dF           Y nw xY wdG}||k     rD| j        r=t+          j        tQ          d||z
                       d{V  |dz  }||k     r| j        =| j        dS dS )Nu  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; disabledNr   r   >   r   r   r   r   zEkanban dispatcher: disabled via HERMES_KANBAN_DISPATCH_IN_GATEWAY envz4kanban dispatcher: cannot load config (%s); disabledr   r   TzGkanban dispatcher: disabled via config kanban.dispatch_in_gateway=falser   z@kanban dispatcher: kanban_db not importable; dispatcher disableddispatch_interval_seconds<   zIkanban dispatcher: invalid dispatch_interval_seconds=%r, using default 60g      N@g      ?	max_spawnzkanban dispatcher: max_spawn=max_in_progressrr   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; ignoringr%      i,  r3   r*   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   )	rM   r*   rK   rL   rF   statOSErrorst_mtime_nsst_size)r3   r   resolvedr   ra   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]   rF   boolc                    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)r)   r{   sqlite3DatabaseErrorr*   r+   )r]   corrupt_guard_errorr   ra   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<r0   '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 dispatchr5   )r6   r   r   r   stale_timeout_secondsr   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)rJ   time	monotonicrC   rz   r   rO   dispatch_oncerU   rF   r  r	  rl   	exception)r3   r\   fingerprintdisabled_entrydisabled_fingerprintdisabled_atager]   !CORRUPT_BOARD_RETRY_AFTER_SECONDSr  r  ra   r   disabled_corrupt_boardsr   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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.
            Fr1   r3   )rE   rF   rG   rH   rJ   rT   )rX   outbr3   ra   r  s       r.   
_tick_oncezIGatewayKanbanWatchersMixin._kanban_dispatcher_watcher.<locals>._tick_onceU  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).
            Fr1   r3   Nr5   T)	rE   rF   rG   rH   rJ   rO   has_spawnable_readyrU   has_spawnable_review)rX   r  r3   r\   ra   s       r.   _ready_nonemptyzNGatewayKanbanWatchersMixin._kanban_dispatcher_watcher.<locals>._ready_nonemptyf  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auto_decomposeauto_decompose_per_tickr"   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); skippingNFr1   r3   HERMES_KANBAN_BOARDz>kanban auto-decompose: list_triage_ids failed on board %s (%s)rr   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'  rF   rC   rv   rE   rG   rH   rJ   rw   rx   list_triage_idsrD   decompose_taskr  okfanout	child_idsrz   rS   rk   r   )_decompr]   rX   	attempted	successesr  r3   prev_env
triage_idstidoutcomera   r%  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 errorg        )r3   r*   r   r   )r]   rF   r   r  )r3   r*   r   r  )r   r  )r   r  )r   r   ))ru   r   rF   rC   rv   rw   rx   rJ   ry   r+   rz   r{   r|   r~   r   r   r   	TypeErrorr   r   DEFAULT_FAILURE_LIMITr   r   r  r   r   reap_worker_zombiesrS   r  r)   r7  	reclaimedhasattrr    r!   promotedauto_blockedr  CancelledErrorrD   min)'rb   r   r   r   r]   r   r   raw_max_in_progressraw_failure_limit	raw_staleraw_per_profileHEALTH_WINDOW	bad_tickslast_warn_atr  r#  auto_decompose_enabledr6  pidsresultsany_spawnedr3   resready_pendingnowsleptr  r  r  ra   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		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	 	 	 	 	 	 	"!	 !	 !	 !	 !	 !	R "&jnn5Et&L&L!M!M	(&)8!<<A' '## :& 	( 	( 	(&'###	("Q&&&'#K	 K	 K	 K	 K	 K	 K	Z 	Ex	
 	
 	
 m ?	L %.s/FGGGGGGGG KKPD		  
  L L L  !JKKKKKL*P) B!+,@AAAAAAAAA ' 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 &   (+)   ;<<< P P P  !NOOOOOP
 E(""t}"mCX-=$>$>????????? (""t}"{ m ?	 ?	 ?	 ?	 ?	s    $A A (
B3 3
C#=CC#E $E54E59%F AG#"G#J .KK'K7 79L32L3;N .N>=N>$Q3 3.R$#R$%T> >UU
A
W $W<;W< F^ A__)r
   )r   r   r   r   r   )r<   r|   r>   r   r6   r   r   r   )r<   r|   r6   r   r   r   )
r<   r|   r   r   r=   r   r6   r   r   r   )r9   r*   rn   r|   rq   r   r   r   )r   r   )
__name__
__module____qualname____doc__r   r   r   r   r   rQ  r   r0   r.   r	   r	      s        IIh' h' h' h' h'V >B    ,    &  $    .k k k kZy y y y y yr0   r	   )rU  
__future__r   r   loggingrw   r  r  r   r   typingr   r   	getLoggerrC   r	   r   r0   r.   <module>rZ     s     # " " " " "   				                         
	=	)	)N N N N N N N N N Nr0   