+
    jHàj¯0  ã                  óè  a € 0 t $ R t^ RIHt ^ RIt^ RIt^ RIt^ RIt^ RIH	t	 ^ RI
Ht ^ RIHtHtHtHtHt ^ RIHt ^ RIHt ^ RIHt RtR	]R
&   R2tR3tRt^Èt]P@                  ! R4      t!]PD                  ! 4       t#R R lt$R R lt%R R lt&]	R R l4       t'R R lt(R R lt)R R lt*R R lt+R R lt,RRR RR!R/R" R# llt-R$ R% lt.R& R' lt/R( R) lt0R* R+ lt1R4R, R- llt2R. R/ lt3R4R0 R1 llt4R# )5u  Durable cron failure incidents with signature dedup and ack.

The executions ledger records every attempt; this module groups the *failures* into incidents keyed
by ``(job_id, error signature)`` so the same job failing with the same error does not re-ping the
operator every run once acknowledged. Lifecycle: ``detected`` â†’ ``alerted`` â†’ ``closed``. The same
job + same normalized error resolves to the SAME incident id, so a closed incident stays closed
until the error text changes and mints a new one. ``alerted`` means a failure ping actually reached
the operator (``alerted_at`` = when the latest one did; the scheduler withholds repeats until
``cron.failure_repeat_alert_hours`` have passed). Incidents share ``cron/executions.db`` with
``cron.executions`` (one ledger file).
)ÚannotationsN)Úcontextmanager)ÚPath)ÚAnyÚDictÚIteratorÚListÚOptional)Ú
executions)Úget_hermes_home)ÚnowzOptional[Path]ÚEXECUTIONS_FILEiô  zJ\b\d+(?:\.\d+)?\s*(?:ms|s|secs?|seconds?|m|mins?|minutes?|h|hrs?|hours?)\bc               ó   € V ^8„  d   QhRR/# )é   Úreturnr   © )Úformats   "Ú3/home/ubuntu/.hermes/hermes-agent/cron/incidents.pyÚ__annotate__r   5   s   € ÷ Bñ B�$ñ Bó    c                 óª   € \         P                  \        3 F  p V f   K	  \        V 4      u # 	  \        4       P	                  4       R,          R,          # )zùShared cron DB path. The ``cron.executions`` override wins when installed so redirecting the
executions ledger also redirects the incident table (they must stay in the SAME database); then
this module's own override, then the canonical profile home.Úcronzexecutions.db)Ú_executionsr   r   r   Úresolve)Úoverrides    r   Ú_db_pathr   5   sH   € ô !×0Ñ0´/ÓBˆØÔÜ˜“>Ò!ñ Cô Ó×$Ñ$Ó&¨Õ/°/ÕAÐAr   c               ó   € V ^8„  d   QhRR/# )r   r   úsqlite3.Connectionr   )r   s   "r   r   r   ?   s   € ÷ 	nñ 	nÐ$ñ 	nr   c                 ór   € ^ RI Hp  ^ RIHp \	        4       pV ! VP
                  4       V! VRR\        R7      # )é    )Ú_ensure_cron_dir)Úopen_dbzcron/executions.dbT)Údb_labelÚsynchronous_fullÚ
initialize)Ú	cron.jobsr    Úhermes_cli.sqlite_utilr!   r   ÚparentÚ_initialize_schema)r    r!   Úpaths      r   Ú_connectr*   ?   s0   € õ +Ý.ä‹:€DÙ�T—[‘[Ô!Ù�4Ð"6ÈÔZlÔmÐmr   c               ó    € V ^8„  d   QhRRRR/# )r   Úconnr   r   ÚNoner   )r   s   "r   r   r   K   s   € ÷ ñ Ð/ð °Dñ r   c                óŽ   € ^ RI Hp V P                  R4       V! V RRR4       V P                  R4       V P                  R4       R# )	r   )Úadd_column_if_missinga   CREATE TABLE IF NOT EXISTS cron_incidents (
             id            TEXT PRIMARY KEY,
             job_id        TEXT NOT NULL,
             error_sig     TEXT NOT NULL,
             state         TEXT NOT NULL,
             failure_type  TEXT NOT NULL DEFAULT 'unknown',
             first_seen_at TEXT NOT NULL,
             last_seen_at  TEXT NOT NULL,
             acked_at      TEXT,
             alerted_at    TEXT,
             closed_at     TEXT,
             error         TEXT NOT NULL,
             output_file   TEXT
           )Úcron_incidentsÚ
alerted_atzalerted_at TEXTzKCREATE INDEX IF NOT EXISTS idx_cron_incidents_job ON cron_incidents(job_id)zLCREATE INDEX IF NOT EXISTS idx_cron_incidents_state ON cron_incidents(state)N)r&   r/   Úexecute)r,   r/   s   & r   r(   r(   K   sL   € Ý<à‡L�Lð	ôñ" ˜$Ð 0°,Ð@QÔRØ‡L�Lð	$ôð 	‡L�Lð	#ör   c               ó   € V ^8„  d   QhRR/# )r   r   zIterator[sqlite3.Connection]r   )r   s   "r   r   r   k   s   € ÷ ñ Ð2ñ r   c            	   #  óà   "  € ^ RI Hp  \        ;_uu_ 4        V ! \        4       4      ;_uu_ 4       pVx € RRR4       RRR4       R#   + '       g   i     L; i  + '       g   i     R# ; i5i)r   )ÚtransactionN)r&   r5   Ú_lockr*   )r5   r,   s     r   Ú_transactionr7   j   s6   é € å2ç	Ž‘œH›J×'Ô'¨4ØŠ
÷ (�‰×'×'ú��ˆüs1   ‚A.—A°A	µA½
A.ÁAÁAÁA+	Á%	A.c               ó    € V ^8„  d   QhRRRR/# ©r   ÚerrorÚstrr   r   )r   s   "r   r   r   r   s   € ÷ Añ A˜Cð A Cñ Ar   c                ó�   € \         P                  ! RR\        T ;'       g    R4      4      P                  4       P	                  4       # )zDStrip whitespace and lowercase before signing (dedup normalization).z\s+Ú Ú )ÚreÚsubr;   ÚstripÚlower)r:   s   &r   Ú_normalize_errorrC   r   s2   € ä�6Š6�&˜#œs 5§; ;¨BÓ/Ó0×6Ñ6Ó8×>Ñ>Ó@Ð@r   c               ó    € V ^8„  d   QhRRRR/# r9   r   )r   s   "r   r   r   w   s   € ÷ 	"ñ 	"˜ð 	" ñ 	"r   c                ó‚   € \        T ;'       g    R4      p ^ RIHp V! VRR7      pVR\         #   \         d     Li ; i)zYRedact secrets (best-effort; the scheduler path never fails on it) then bound the length.r>   )Úredact_sensitive_textT)ÚforceN)r;   Úagent.redactrF   Ú	ExceptionÚMAX_ERROR_CHARS)r:   ÚtextrF   s   &  r   Ú_redact_errorrL   w   sJ   € äˆu�{ˆ{˜Ó€DðÝ6á$ T°Ô6ˆð Ð ”Ð!Ð!øô ô Ùðús   —0 °>½>c               ó$   € V ^8„  d   QhRRRRRR/# )r   Újob_idr;   r:   r   r   )r   s   "r   r   r   ƒ   s&   € ÷ Rñ R˜Sð R¨ð R°ñ Rr   c                óê   € \         P                  R\        V4      4      R\         p\        P
                  ! V P                  4       VP                  4       ,           4      P                  4       R,          # )z>Dedup key: stable for same job + same normalized error prefix.z#sN:Né   N)Ú_DURATION_REr@   rC   Ú_MAX_SIGNATURE_ERROR_CHARSÚhashlibÚsha256ÚencodeÚ	hexdigest)rN   r:   Ú
normalizeds   && r   Ú_error_signaturerX   ƒ   sT   € ä×!Ñ! $Ô(8¸Ó(?Ó@ÐA\ÔB\Ð]€JÜ�>Š>˜&Ÿ-™-›/¨J×,=Ñ,=Ó,?Õ?Ó@×JÑJÓLÈSÕQÐQr   c               ó$   € V ^8„  d   QhRRRRRR/# )r   rN   r;   Ú	error_sigr   r   )r   s   "r   r   r   ‰   s!   € ÷ 'ñ '˜ð '¨ð '°ñ 'r   c                ó   € V R ,           RV 2# ):Né   NÚ_r   )rN   rZ   s   &&r   Ú_incident_idr^   ‰   s   € Ø�R�jˆ\˜˜9˜+Ð&Ð&r   c               ó    € V ^8„  d   QhRRRR/# r9   r   )r   s   "r   r   r   �   s   € ÷ ñ  #ð ¨#ñ r   c                ó  € \        V 4      pV'       g   R# \         Fl  w  r#V Fa  pVP                  R4      '       d<   VP                  R4      '       d%   \        P
                  ! WA4      '       d   Vu u # KU  WA9   g   K]  Vu u # 	  Kn  	  R# )zHClassify a failure from error-text keywords; ``unknown`` is the default.Úunknownz\b)rC   Ú_FAILURE_TYPE_ORDERÚ
startswithÚendswithr?   Úsearch)r:   rK   ÚkindÚpatternsÚpatterns   &    r   Ú_classify_failure_typeri   �   sq   € ä˜EÓ"€DßÙß-‰ˆÛˆGØ×!Ñ! %×(Ò(¨W×-=Ñ-=¸e×-DÒ-DÜ—9’9˜W×+Ò+Ø”Kñ ,à–Ø”ó  ñ .ñ r   Újob_nameÚfailure_typeÚoutput_filec               ó0   € V ^8„  d   QhRRRRRRRRRRRR	/# )
r   rN   r;   r:   rj   úOptional[str]rk   rl   r   ztuple[str, bool]r   )r   s   "r   r   r   œ   s>   € ÷ )!ñ )!Øð)!Øð)!Ø*7ð)!ØN[ð)!àð)!ð ñ)!r   c               ó$  € \        T ;'       g    R4      p \        W4      p\        V4      p\        W4      p\	        4       P                  4       pT;'       g    \        V4      pVe   \        V4      MRp\        4       ;_uu_ 4       p	V	P                  RV34      P                  4       p
V
e.   V
R,          R8H  pV	P                  RW†WG34       W{3uuRRR4       # V	P                  RWpWSWˆWd34       VR3uuRRR4       #   + '       g   i     R# ; i)	u¤  Record (or refresh) the incident for ``job_id`` + ``error``; returns ``(incident_id,
is_new)``. An existing row for the signature refreshes
``last_seen_at``/``error``/``output_file`` and keeps its state â€” a ``closed`` incident stays
closed, while a ``resolved`` one (job recovered, then broke the same way again) re-opens as
``detected`` so the operator is alerted once more. A changed error text mints a new incident.r>   Nz/SELECT id, state FROM cron_incidents WHERE id=?ÚstateÚresolveda  UPDATE cron_incidents
                   SET last_seen_at=?, error=?, output_file=?,
                       state=CASE WHEN state='resolved' THEN 'detected' ELSE state END,
                       alerted_at=CASE WHEN state='resolved' THEN NULL ELSE alerted_at END,
                       closed_at=CASE WHEN state='resolved' THEN NULL ELSE closed_at END
                   WHERE id=?zÒINSERT INTO cron_incidents
               (id, job_id, error_sig, state, failure_type,
                first_seen_at, last_seen_at, error, output_file)
               VALUES (?, ?, ?, 'detected', ?, ?, ?, ?, ?)T)
r;   rX   rL   r^   Ú_hermes_nowÚ	isoformatri   r7   r2   Úfetchone)rN   r:   rj   rk   rl   ÚsigÚstored_errorÚincident_idr   r,   ÚrowÚreopens   &&$$$       r   Úupsert_incidentrz   œ   s  € ô �—�˜2Ó€FÜ
˜6Ó
)€CÜ  Ó'€LÜ˜vÓ+€KÜ
‹-×
!Ñ
!Ó
#€CØ×@Ð@Ô#9¸%Ó#@€LØ&1Ò&=”#�kÔ"À4€Kä	�Œ˜4Ø�l‰lØ=À¸~ó
ç
‰(‹*ð 	ð Š?Ø˜•\ ZÑ/ˆFØ�L‰Lð!ð  KÐ=ôð Ð&÷ 
Šð  	�‰ð>ð  #°SØð(ô	
ð ˜DÐ ÷1 
��‹ús   ÂAC>ÃC>Ã>D	c               ó$   € V ^8„  d   QhRRRRRR/# )r   rw   r;   rp   r   Úboolr   )r   s   "r   r   r   È   s!   € ÷ "ñ " Cð "°ð "¸ñ "r   c                ó  € V\         9  d   R# \        4       P                  4       p\        4       ;_uu_ 4       pVP	                  RV 34      P                  4       pVe   VR,          R8X  d    RRR4       R# VR8X  d   VP	                  RW 34        RRR4       R# VR,          V8X  d    RRR4       R# VR8X  d   VP	                  R	W"V 34       MVP	                  R
W34        RRR4       R#   + '       g   i     R# ; i)uv  Transition an incident's lifecycle state; return whether it changed. ``closed`` is terminal
for that signature (re-open happens by a changed error minting a NEW incident). Unknown states
are rejected (no-op, ``False``). ``alerted`` also stamps ``alerted_at`` â€” every time, so the
cooldown reminder (see ``cron.scheduler._upsert_incident_for_failure``) restarts its window.Fz+SELECT state FROM cron_incidents WHERE id=?Nrp   ÚclosedÚalertedzBUPDATE cron_incidents SET state='alerted', alerted_at=? WHERE id=?TzˆUPDATE cron_incidents
                   SET state='closed', closed_at=?, acked_at=?
                   WHERE id=? AND state != 'closed'z,UPDATE cron_incidents SET state=? WHERE id=?)ÚINCIDENT_STATESrr   rs   r7   r2   rt   )rw   rp   r   r,   rx   s   &&   r   Úset_incident_stater�   È   só   € ð
 ”OÔ#ÙÜ
‹-×
!Ñ
!Ó
#€CÜ	�Œ˜4Ø�l‰lØ9¸K¸>ó
ç
‰(‹*ð 	ð Š;˜#˜g�,¨(Ô2Ø÷ 
‰ð �IÔØ�L‰LØTØÐ"ôð ÷ 
‰ð ˆw�<˜5Ô Ø÷ 
‰ð �HÔØ�L‰Lð7ð ˜;Ð'õ	ð �L‰LØ>ØÐ$ôð ÷5 
��Šús   ¸5C/Á7C/ÂC/Â50C/Ã/D 	c               ó    € V ^8„  d   QhRRRR/# )r   rw   r;   r   r|   r   )r   s   "r   r   r   í   s   € ÷ 5ñ 5˜cð 5 dñ 5r   c                ó   € \        V R4      # )zJAcknowledge (close) an incident; ``False`` when missing or already closed.r~   )r�   )rw   s   &r   Úack_incidentr„   í   s   € ä˜k¨8Ó4Ð4r   c               ó    € V ^8„  d   QhRRRR/# )r   rN   r;   r   Úintr   )r   s   "r   r   r   ò   s   € ÷ )ñ )¨cð )°cñ )r   c           
     ó   € \        4       P                  4       p\        4       ;_uu_ 4       pTP                  RT\	        T ;'       g    R4      34      p\        VP                  ;'       g    ^ 4      uuRRR4       #   + '       g   i     R# ; i)a\  Mark every open incident for ``job_id`` ``resolved`` after a successful run; returns how many.
Without this the ledger only ever grows: a one-off failure (a config drift skip, a provider
outage) stayed ``detected``/``alerted`` forever after the job recovered, so ``hermes cron
incidents`` showed dozens of "open" incidents for jobs that had been green for weeks (32 of 32 on
one install). ``resolved`` is distinct from the operator's ``closed`` on purpose: a repeat of the
same error re-opens a resolved incident and alerts again (see ``upsert_incident``), whereas
``closed`` keeps that signature silent.zzUPDATE cron_incidents SET state='resolved', closed_at=?
               WHERE job_id=? AND state IN ('detected', 'alerted')r>   N)rr   rs   r7   r2   r;   r†   Úrowcount)rN   r   r,   Úcursors   &   r   Ú!close_incidents_for_recovered_jobrŠ   ò   sf   € ô ‹-×
!Ñ
!Ó
#€CÜ	�Œ˜4Ø—‘ðFà”#�f—l�l Ó#Ð$ó
ˆô
 �6—?‘?×'Ð' aÓ(÷ 
��‹ús   «?A<Á+A<Á<B	c               ó    € V ^8„  d   QhRRRR/# )r   rp   rn   r   ztuple[str, tuple]r   )r   s   "r   r   r     s   € ÷ Gñ G˜ð GÐ+<ñ Gr   c                ó   € V f   R# RV 33# )Nz WHERE state=?)r>   r   r   )rp   s   &r   Ú_state_filterr�     s   € Ø’}ˆ8ÐFÐ+;¸e¸XÐ*FÐFr   c               ó    € V ^8„  d   QhRRRR/# )r   rp   rn   r   zlist[dict[str, Any]]r   )r   s   "r   r   r     s   € ÷ 'ñ '˜-ð 'Ð3Gñ 'r   c                ó6  € V e   V \         9  d   . # \        V 4      w  r\        4       ;_uu_ 4       pVP                  RV,           R,           V4      P	                  4       pRRR4       X Uu. uF  p\        V4      NK  	  up#   + '       g   i     L+; iu upi )zFReturn incidents, newest-activity first, optionally filtered by state.NzSELECT * FROM cron_incidentszB ORDER BY julianday(last_seen_at) DESC, last_seen_at DESC, id DESC)r€   r�   r7   r2   ÚfetchallÚdict)rp   ÚwhereÚparamsr,   Úrowsrx   s   &     r   Úlist_incidentsr•     sƒ   € àÒ˜U¬/Ô9Øˆ	Ü! %Ó(�M€EÜ	�Œ˜4à�|‰|Ø*¨UÕ2ØRõSàó
÷ ‰(‹*ð	 	÷ 
ñ "&Ó&¡˜#ŒD�ŽI¡Ñ&Ð&÷ 
�üò 's   ±/BÁ,BÂB	c               ó    € V ^8„  d   QhRRRR/# )r   rw   r;   r   zOptional[dict[str, Any]]r   )r   s   "r   r   r     s   € ÷ 2ñ 2˜cð 2Ð&>ñ 2r   c                óÂ   € \        4       ;_uu_ 4       pVP                  R V 34      P                  4       pRRR4       Xe   \        V4      # R#   + '       g   i     L!; i)z'SELECT * FROM cron_incidents WHERE id=?N)r7   r2   rt   r‘   )rw   r,   rx   s   &  r   Úget_incidentr˜     sL   € Ü	�Œ˜4Ø�l‰lØ5¸°~ó
ç
‰(‹*ð 	÷ 
ð šŒ4�‹9Ð1¨TÐ1÷	 
�ús   “"AÁA	c               ó    € V ^8„  d   QhRRRR/# )r   rp   rn   r   r†   r   )r   s   "r   r   r     s   € ÷ 3ñ 3˜=ð 3°Cñ 3r   c                ó  € V e   V \         9  d   ^ # \        V 4      w  r\        4       ;_uu_ 4       pVP                  RV,           V4      P	                  4       pR R R 4       Xe   \        VR,          4      # ^ #   + '       g   i     L(; i)Nz(SELECT COUNT(*) AS n FROM cron_incidentsÚn)r€   r�   r7   r2   rt   r†   )rp   r’   r“   r,   rx   s   &    r   Úcount_incidentsrœ     sj   € ØÒ˜U¬/Ô9ÙÜ! %Ó(�M€EÜ	�Œ˜4Ø�l‰lÐEÈÕMÈvÓV×_Ñ_Óaˆ÷ 
àšOŒ3ˆs�3�x‹=Ð2°Ð2÷ 
�ús   ±(A9Á9B		)Údetectedr   rq   r~   ))Ú
rate_limit)z\b429\bz
rate limitzusage limitÚquota)Útimeout)r    z	timed out)Úauth)z\b401\bÚunauthorizedÚauthenticationr¡   )Údelivery)r¤   ÚdeliverÚ
delivering)Úconfig)r§   ÚconfigurationÚ
validation)Úscript)rª   Úno_agent)Úagent)r¬   ÚmodelÚproviderÚ	inference)N)5Ú__conditional_annotations__Ú__doc__Ú
__future__r   rS   r?   Úsqlite3Ú	threadingÚ
contextlibr   Úpathlibr   Útypingr   r   r   r   r	   r   r
   r   Úhermes_constantsr   Úhermes_timer   rr   r   Ú__annotations__r€   rb   rJ   rR   ÚcompilerQ   ÚRLockr6   r   r*   r(   r7   rC   rL   rX   r^   ri   rz   r�   r„   rŠ   r�   r•   r˜   rœ   )r°   s   @r   Ú<module>r½      s  øðô
õ #ã Û 	Û Û Ý %Ý ß 6Õ 6å *Ý ,Ý *ð #'€�Ó &ð @€ðÐ ð €Ø Ð ð �zŠzØQó€ð 	�ŠÓ€õBõ	nõð> ôó ðõAõ
	"õRõ'õð)!Ø:>ð)!Ø^bð)!à!%÷)!õX"õJ5õ
)õ$G÷'õ2÷3ñ 3r   