§
    øžyj� ã            +       ó~  — U d Z ddlZddlZddlmZ ddlmZ ddlZddlZddl	Z	ddl
Z
ddlZddlZddlZddlZddlZ	 ddlZn# e$ r dZY nw xY w	 ddlZn# e$ r dZY nw xY wddlmZmZ ddlmZ ddlmZ ddlmZmZmZmZmZmZm Z m!Z!  ej"        e#¦  «        Z$dd	l%m&Z' dd
l(m)Z)m*Z* da+da,ee-         e.d<   de-fd„Z/ e¦   «          0                    ¦   «         Z1e1dz  Z2e2dz  Z3e2dz  Z4e2dz  Z5dZ6 ej7        ¦   «         Z8 ej9        ¦   «         Z:dZ;e2dz  Z<dZ= ed¬¦  «         G d„ d¦  «        ¦   «         Z> edd¬¦  «        Z?eee>                  e.d<    e>e2e3e<¦  «        Z@de>fd„ZAejB        de eCef         fd„¦   «         ZDdefd „ZEd!ZFd"ZGd#ZHdeIfd$„ZJd%eCde-fd&„ZKdefd'„ZLejB        d(„ ¦   «         ZM eNd)h¦  «        ZOd%eCdefd*„ZPd·d+eeC         d,ee         deeC         fd-„ZQd.eeCef         deeCef         fd/„ZRd¸d1ed2eCdeCfd3„ZSd.eeCef         deCfd4„ZTd.eeCef         deeCef         fd5„ZUd.eeCef         de-fd6„ZVd.eeCef         de-fd7„ZWd.eeCef         deCfd8„ZXd9efd:„ZYd9efd;„ZZd9ed<eej[                 ddfd=„Z\d>„ Z]d?eCde^fd@„Z_dAeCdeeCef         fdB„Z`dCedefdD„ZadEedFede-fdG„ZbdEedFede-fdH„ZcddIœdAeeCef         dJedKeeC         deeC         fdL„ZddAeede^fdM„Zfd¹dAeeCef         dKeeC         deeC         fdN„Zgd9eddfdO„Zhd9ed1e^ddfdP„ZidºdRe-ddfdS„Zjd9edeeI         fdT„ZkdeeI         fdU„ZldeeI         fdV„Zmd»dW„ZndXeCddfdY„Zode^fdZ„Zpd»d[„ZqdeeC         fd\„Zrd]edeee-f         fd^„ZsdeeeCef                  fd_„ZtdeeeeCef                           fd`„Zud]edeee^e^e^f                  fda„Zvdbeee^e^e^f                  ddfdc„ZwdddœdeeeeCef                  dfee!eC                  deeeCef                  fdg„ZxddQdhœdeeeeCef                  dfee!eC                  die-fdj„ZyddQdhœdeeeeCef                  dfee!eC                  die-fdk„ZzdleeC         deeC         fdm„Z{deeC         fdn„Z|dQdoœd1edpe-deeC         fdq„Z}dredsedteduedeeeC         eeC         f         f
dv„Z~d.eeCef         deeeC         eeC         eeC         e-f         fdw„ZdxeeC         dyeeC         due-dzeeC         ddf
d{„Z€	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d¼d|eeC         dAeCd}eeC         d~ee^         deeC         d€eeeCef                  d+eeC         d,eeeC                  dseeC         dreeC         dteeC         dzeeC         d�ee eCeeC         f                  d‚eeeC                  dleeC         due-dƒee-         dxeeC         dyeeC         deeCef         f(d„„Z�d%eCdeeeCef                  fd…„Z‚ G d†„ d‡eƒ¦  «        Z„dˆeCdeeeCef                  fd‰„Z…dºdŠe-deeeCef                  fd‹„Z†d%eCdŒeeCef         deeeCef                  fd�„Z‡d¹d%eCdŽeeC         deeeCef                  fd�„Zˆd%eCdeeeCef                  fd�„Z‰d%eCdeeeCef                  fd‘„ZŠd%eCde-fd’„Z‹d%eCd1e-de-fd“„ZŒd%eCde-fd”„Z�d%eCddfd•„ZŽ	 	 	 d½d%eCdRe-d–eeC         d—eeC         d˜eeC         f
d™„Z�d.eeCef         ddfdš„Z�d%eCde-fd›„Z‘d%eCdœeCde-fd�„Z’de^fdž„Z“d%eCde-fdŸ„Z”deCfd „Z•d¡d¢œd%eCd£e^de-fd¤„Z–d¥Z—deIfd¦„Z˜dddœd§eeeCef                  dJedfeeeC                  de-fd¨„Z™deeeCef                  fd©„ZšdeeeCef                  fdª„Z›d«Zœde^fd¬„Z�d­ed®e^de^fd¯„Zžd%eCdeCfd°„ZŸd±edeCfd²„Z deeC         fd³„Z¡	 	 d·d´eeeCeCf                  dµeeeC                  deeCef         fd¶„Z¢dS )¾z�
Cron job storage and management.

Jobs are stored in ~/.hermes/cron/jobs.json
Output is saved to ~/.hermes/cron/output/{job_id}/{timestamp}.md
é    N)Ú
ContextVar)Ú	dataclass)ÚdatetimeÚ	timedelta)ÚPath)Úget_hermes_home)ÚOptionalÚDictÚListÚAnyÚSetÚTupleÚUnionÚ
Collection)Únow)Úatomic_replaceÚatomic_write_textÚHAS_CRONITERÚreturnc                  ót   — t           €	 ddlm}  | ada n# t          $ r da Y nw xY wt          t           ¦  «        S )zDImport croniter on first use; honor a pre-set HAS_CRONITER override.Nr   )ÚcroniterTF)r   r   ÚImportErrorÚbool)Ú	_croniters    ú//home/ragecks/.hermes/hermes-agent/cron/jobs.pyÚ_ensure_croniterr   4   s_   € õ Ðð	!Ø6Ð6Ð6Ð6Ð6Ð6Ø ˆHØˆLˆLøÝð 	!ð 	!ð 	!Ø ˆLˆLˆLð	!øøøå•ÑÔÐs   ‰
 ”#¢#Úcronú	jobs.jsonÚticker_heartbeatÚticker_last_successé<   g      >@Úoutputéx   T)Úfrozenc                   ó.   — e Zd ZU eed<   eed<   eed<   dS )Ú_CronStorePathsÚcron_dirÚ	jobs_fileÚ
output_dirN)Ú__name__Ú
__module__Ú__qualname__r   Ú__annotations__© ó    r   r&   r&   w   s0   € € € € € € à€N€N�NØ€O€O�OØÐÐÑÐÐr/   r&   Úcron_store_override)ÚdefaultÚ_cron_store_overridec                  ó(  — t                                ¦   «         } | �| S t          t          t          t
          ¦  «        }|t          k    r|S t          ¦   «                              ¦   «         }|t          k    r|S |dz  }t          ||dz  |dz  ¦  «        S )u’  Return paths pinned to this execution context's profile.

    Precedence, most explicit first:

    1. an active use_cron_store() override (ContextVar);
    2. deliberately re-pointed module constants â€” if CRON_DIR/JOBS_FILE/
       OUTPUT_DIR no longer match their import-time values, someone chose
       the documented process-wide compatibility surface; honor it;
    3. the ACTIVE profile home, resolved fresh via get_hermes_home()
       (context-local override, then the HERMES_HOME env var) â€” so a test
       or embedder that re-points HERMES_HOME after this module was
       imported reads/writes ITS OWN store, not whatever jobs.json the
       import happened to freeze (the filed incident: fixtures that patched
       the env too late silently rewrote the user's real jobs file);
    4. the import-time constants (home unchanged since import â€” the common
       path, returned unchanged).
    Nr   r   r"   )
r2   Úgetr&   ÚCRON_DIRÚ	JOBS_FILEÚ
OUTPUT_DIRÚ_IMPORT_STOREr   ÚresolveÚ
HERMES_DIR)ÚoverrideÚlive_constantsÚhomer'   s       r   Ú_current_cron_storer>   ‹   s�   € õ$ $×'Ò'Ñ)Ô)€HØÐØˆÝ$¥X­y½*ÑEÔE€NØ�Ò&Ð&ØÐÝÑÔ×$Ò$Ñ&Ô&€DØ�zÒÐØÐØ�f‰}€HÝ˜8 X°Ñ%;¸XÈÑ=PÑQÔQÐQr/   r=   c              #   óR  K  — t          | ¦  «                             ¦   «                              ¦   «         dz  }t                               t          ||dz  |dz  ¬¦  «        ¦  «        }	 dV — t                               |¦  «         dS # t                               |¦  «         w xY w)z@Route cron storage to ``home`` without mutating process globals.r   r   r"   )r'   r(   r)   N)r   Ú
expanduserr9   r2   Úsetr&   Úreset)r=   r'   Útokens      r   Úuse_cron_storerD   ª   s­   è è € õ �D‰zŒz×$Ò$Ñ&Ô&×.Ò.Ñ0Ô0°6Ñ9€HÝ ×$Ò$ÝØØ Ñ,Ø (Ñ*ð	
ñ 	
ô 	
ñô €Eð*Øˆˆˆå×"Ò" 5Ñ)Ô)Ð)Ð)Ð)øÕ×"Ò" 5Ñ)Ô)Ð)Ð)øøøs   Á*B
 Â
B&c                  ó(   — t          ¦   «         j        S )z>Return the output directory for the active cron store context.)r>   r)   r.   r/   r   Úget_cron_output_dirrF   »   s   € åÑ Ô Ô+Ð+r/   i  é   g     À‚@c                  óH  — t          j        dd¦  «                             ¦   «         } t          }| r/	 t	          | ¦  «        }n# t
          t          f$ r
 t          }Y nw xY w|dk    rt	          t          ¦  «        S t          |t          z  t	          t          ¦  «        ¦  «        S )ug  Resolve the one-shot running-claim stale-recovery TTL.

    Derived from ``HERMES_CRON_TIMEOUT`` (the cron inactivity timeout the
    scheduler enforces on each run) so the safety valve tracks how long a run
    is actually allowed to go quiet, instead of a magic constant:

    - unset / invalid â†’ default 600s inactivity limit â†’ TTL = 1800s
    - ``0`` (unlimited runs) â†’ no finite bound to derive from â†’ fall back to
      ``ONESHOT_RUN_CLAIM_TTL_SECONDS``
    - positive N â†’ ``max(N * headroom, ONESHOT_RUN_CLAIM_TTL_SECONDS)`` so a
      tiny configured timeout can never expire a claim mid-run.
    ÚHERMES_CRON_TIMEOUTÚ r   )
ÚosÚgetenvÚstripÚ _DEFAULT_CRON_INACTIVITY_TIMEOUTÚfloatÚ
ValueErrorÚ	TypeErrorÚONESHOT_RUN_CLAIM_TTL_SECONDSÚmaxÚ_ONESHOT_RUN_CLAIM_TTL_HEADROOM)ÚrawÚtimeouts     r   Ú_oneshot_run_claim_ttl_secondsrW   Ò   s©   € õ Œ)Ð)¨2Ñ
.Ô
.×
4Ò
4Ñ
6Ô
6€CÝ.€GØ
ð 7ð	7Ý˜C‘j”jˆGˆGøÝ�IÐ&ð 	7ð 	7ð 	7Ý6ˆGˆGˆGð	7øøøà�!‚|€|åÕ2Ñ3Ô3Ð3ÝØÕ1Ñ1ÝÕ+Ñ,Ô,ñô ð s   ²A ÁAÁAÚjob_idc                 ó„   — 	 ddl m} |  |¦   «         v S # t          $ r! t                               d| d¬¦  «         Y dS w xY w)u|  Return True when the scheduler in THIS process is still running ``job_id``.

    Direct liveness signal for stale-entry recovery (#62002): the run_claim
    TTL alone cannot distinguish "the claiming tick died" from "the run is
    alive but slow" â€” a run stalled on network I/O (or a laptop that slept
    mid-run) legitimately outlives the TTL. The in-process ticker and the run
    share this process, so the scheduler's running set settles the common
    single-gateway case without any claim-age guesswork.

    Imported lazily: the scheduler imports this module at load, so a
    module-level import here would be circular.
    r   )Úget_running_job_idszsCron running-set liveness check failed for job %r; keeping the entry to avoid deleting a possibly live one-shot runT©Úexc_info)Úcron.schedulerrZ   Ú	ExceptionÚloggerÚwarning)rX   rZ   s     r   Ú_job_running_in_this_processra   ï   sy   € ð
Ø6Ð6Ð6Ð6Ð6Ð6ØÐ,Ð,Ñ.Ô.Ð.Ð.øÝð ð ð Ý�ŠðCàØð	 	ñ 	
ô 	
ð 	
ð ˆtˆtðøøøs   ‚ ”'?¾?c                  ó.   — t          ¦   «         j        dz  S )z=Return the advisory lock path for the current cron directory.z
.jobs.lock)r>   r'   r.   r/   r   Ú_jobs_lock_filerc   	  s   € åÑ Ô Ô)¨LÑ8Ð8r/   c            
   #   ó  K  — t          t          dd¦  «        } | rF| dz   t          _        	 dV — t          xj        dz  c_        n# t          xj        dz  c_        w xY wdS t          5  dt          _        dt          _        d}	 	 t          ¦   «          t          t          ¦   «         dd¬¦  «        }|                     d¦  «         t          �åt          j        ¦   «         t          z   }	 	 t          j        |t          j        t          j        z  ¦  «         nš# t           t"          f$ r… t          j        ¦   «         |k    rWt$                               d	t          t          ¦   «         ¦  «         	 |                     ¦   «          n# t           $ r Y nw xY wd}Y nt          j        d
¦  «         Y nw xY wŒÈnLt,          �E t          t,          d¦  «        |                     ¦   «         t          t,          d¦  «        d¦  «         n9# t           t"          f$ r%}t$                               d|¦  «         Y d}~nd}~ww xY w	 dV — |�º	 t          � t          j        |t          j        ¦  «         nLt,          �E t          t,          d¦  «        |                     ¦   «         t          t,          d¦  «        d¦  «         n# t           t"          f$ r Y nw xY w|                     ¦   «          nÛ# |                     ¦   «          w xY wnÁ# |�º	 t          � t          j        |t          j        ¦  «         nLt,          �E t          t,          d¦  «        |                     ¦   «         t          t,          d¦  «        d¦  «         n# t           t"          f$ r Y nw xY w|                     ¦   «          w # |                     ¦   «          w xY ww xY wdt          _        dt          _        n# dt          _        dt          _        w xY w	 ddd¦  «         dS # 1 swxY w Y   dS )uð  Serialize a load_jobsâ†’modifyâ†’save_jobs critical section.

    Combines the in-process threading lock (cheap mutual exclusion between
    the gateway's parallel tick threads) with a cross-process advisory file
    lock on ``<cron dir>/.jobs.lock`` (mutual exclusion between the gateway process
    and standalone ``hermes`` CLI invocations, which previously shared no lock
    at all â€” a `cron pause` could be silently clobbered by a concurrent
    gateway write, leaving a "paused" job still firing).

    The flock is blocking, but every critical section that uses it is short
    (field updates only â€” no agent execution), so contention resolves in
    milliseconds. If neither fcntl nor msvcrt is available the manager still
    provides in-process locking, matching the historical behaviour.

    Nested calls in the same thread reuse the held lock so legacy callers that
    invoke save_jobs() inside a broader mutation section don't deadlock or try
    to reacquire the advisory file lock.
    Údepthr   é   Nza+úutf-8©ÚencodingTu«   Timed out after %.0fs waiting for the cron jobs lock (%s) â€” another process is holding it. Proceeding with in-process locking only so the scheduler stays alive (#60703).gš™™™™™¹?ÚlockingÚLK_LOCKzSjobs.json cross-process lock unavailable (%s); proceeding with in-process lock onlyÚLK_UNLCK)ÚgetattrÚ_jobs_lock_statere   Ú_jobs_file_lockÚ
load_stampÚensure_dirsÚopenrc   ÚseekÚfcntlÚtimeÚ	monotonicÚ_JOBS_LOCK_TIMEOUT_SECONDSÚflockÚLOCK_EXÚLOCK_NBÚOSErrorÚIOErrorr_   ÚerrorÚcloseÚsleepÚmsvcrtÚfilenor`   ÚLOCK_UN)re   Úlock_fdÚ	_deadlineÚes       r   Ú
_jobs_lockr†     sš  è è € õ( Õ$ g¨qÑ1Ô1€EØð Ø!&¨¡ÕÔð	(ØˆEˆEˆEåÐ"Ô" aÑ'Ð"Ô"Ð"øÕÐ"Ô" aÑ'Ð"Ô"Ð"Ð"Ð"Ð"Øˆå	ð I/ð I/Ø!"ÕÔð '+ÕÔ#Øˆð@	/ð/JÝ‘”�Ý�Ñ0Ô0°$ÀÐIÑIÔI�Ø—’˜Q‘”�ÝÐ$õ !%¤Ñ 0Ô 0Õ3MÑ M�Ið,ð,Ý!œK¨µ´ÅÄÑ1NÑOÔOÐOØ!øÝ '­Ð1ð ,ð ,ð ,Ý#œ~Ñ/Ô/°9Ò<Ð<Ý &§¢ð%Mõ %?Ý$3Ñ$5Ô$5ñ!"ô !"ð !"ð!)Ø$+§M¢M¡O¤O O OøÝ'.ð !)ð !)ð !)Ø$( Dð!)øøøà*. Ø % Ý œJ s™OœO˜O˜O˜Oð!,øøøð	,øõ* Ð'Ø.•G�F IÑ.Ô.¨w¯~ª~Ñ/?Ô/?ÅÍÐQZÑA[ÔA[Ð]^Ñ_Ô_Ð_øøÝ�WÐ%ð Jð Jð Jõ —’ð  FØGHñJô Jð Jð Jð Jð Jð Jð JøøøøðJøøøð
(Ø���àÐ&ð(Ý Ð,Ý!œK¨µ´Ñ?Ô?Ð?Ð?Ý#Ð/Ø6�G¥F¨IÑ6Ô6°w·~²~Ñ7GÔ7GÍÕQWÐYcÑIdÔIdÐfgÑhÔhÐhøøÝ#¥WÐ-ð ð ð Ø˜ðøøøð  Ÿš™œ˜˜ø˜Ÿš™œ˜˜øøøð 'ø�7Ð&ð(Ý Ð,Ý!œK¨µ´Ñ?Ô?Ð?Ð?Ý#Ð/Ø6�G¥F¨IÑ6Ô6°w·~²~Ñ7GÔ7GÍÕQWÐYcÑIdÔIdÐfgÑhÔhÐhøøÝ#¥WÐ-ð ð ð Ø˜ðøøøð  Ÿš™œ˜˜ø˜Ÿš™œ˜˜øøøð 'øøøð &'ÕÔ"Ø*.ÕÔ'Ð'øð &'ÕÔ"Ø*.ÕÔ'Ð.Ð.Ð.Ð.Ð'ðSI/ð I/ð I/ñ I/ô I/ð I/ð I/ð I/ð I/ð I/ð I/ð I/øøøð I/ð I/ð I/ð I/ð I/ð I/s6  «A ÁAÁ(O?ÂA$G?Ã*,DÄG?ÄAF-Å-FÆF-Æ
FÆF-ÆFÆF-ÆG?ÆF-Æ*G?Æ,F-Æ-AG?Ç>OÇ?H5ÈH0È+OÈ0H5È5OÈ9K:È=OÉ A3J4Ê3K Ê4KËK ËKËK ËOË K6Ë6OË:N8Ë>A3M2Í1NÍ2N	ÎNÎN	ÎNÎ	N8ÎN4	Î4N8Î8OÎ;O?ÏO.Ï.O?Ï?PÐPÚidc                 óB  — t          | pd¦  «                             ¦   «         }|r|dv sd|v sd|v rt          d| ›�¦  «        ‚t          |¦  «                             ¦   «         st          |¦  «        j        rt          d| ›�¦  «        ‚t          ¦   «         j        |z  S )ad  Resolve a job's output directory, rejecting any path-escape attempt.

    Job IDs are filesystem path components under ``OUTPUT_DIR``. A legacy or
    crafted ID containing ``..``, absolute paths, or nested separators would
    allow output writes/deletes to escape the cron output sandbox. Reject
    anything that isn't a single safe path component.
    rJ   >   ú..ú.ú/ú\z%Invalid cron job id for output path: )ÚstrrM   rP   r   Úis_absoluteÚdriver>   r)   )rX   Útexts     r   Ú_job_output_dirr‘   }  s²   € õ ˆvˆ|˜ÑÔ×"Ò"Ñ$Ô$€DØð M�4˜;Ð&Ð&¨#°¨+¨+¸À¸¸ÝÐKÀÐKÐKÑLÔLÐLÝˆD�z„z×ÒÑÔð M¥4¨¡:¤:Ô#3ð MÝÐKÀÐKÐKÑLÔLÐLÝÑ Ô Ô+¨dÑ2Ð2r/   ÚskillÚskillsc                 óô   — |€| r| gng }n(t          |t          ¦  «        r|g}nt          |¦  «        }g }|D ]@}t          |pd¦  «                             ¦   «         }|r||vr|                     |¦  «         ŒA|S )zPNormalize legacy/single-skill and multi-skill inputs into a unique ordered list.NrJ   )Ú
isinstancer�   ÚlistrM   Úappend)r’   r“   Ú	raw_itemsÚ
normalizedÚitemr�   s         r   Ú_normalize_skill_listr›   �  s›   € à€~Ø$Ð,�U�G�G¨"ˆ	ˆ	Ý	�F�CÑ	 Ô	 ð !Ø�Hˆ	ˆ	å˜‘L”Lˆ	à€JØð $ð $ˆÝ�4�:˜2‰Œ×$Ò$Ñ&Ô&ˆØð 	$�D 
Ð*Ð*Ø×Ò˜dÑ#Ô#Ð#øØÐr/   Újobc                 ó¸   — t          | ¦  «        }t          |                     d¦  «        |                     d¦  «        ¦  «        }||d<   |r|d         nd|d<   |S )zLReturn a job dict with canonical `skills` and legacy `skill` fields aligned.r’   r“   r   N)Údictr›   r4   )rœ   r™   r“   s      r   Ú_apply_skill_fieldsrŸ   ž  s[   € å�c‘”€JÝ" :§>¢>°'Ñ#:Ô#:¸J¿NºNÈ8Ñ<TÔ<TÑUÔU€FØ!€JˆxÑØ'-Ð7˜& œ)˜)°4€JˆwÑØÐr/   rJ   ÚvalueÚfallbackc                 ó(   — | €|S t          | ¦  «        S )zFCoerce legacy/hand-edited nullable cron fields to strings for readers.©r�   )r    r¡   s     r   Ú_coerce_job_textr¤   §  s   € à€}ØˆÝˆu‰:Œ:Ðr/   c                 ól  — t          |                      d¦  «        ¦  «                             ¦   «         }|r|S |                      d¦  «        }t          |t          ¦  «        r@dD ]<}t          |                     |¦  «        ¦  «                             ¦   «         }|r|c S Œ=n|�t          |¦  «        S dS )NÚschedule_displayÚschedule)Údisplayr    ÚexprÚrun_atú?)r¤   r4   rM   r•   rž   r�   )rœ   r¨   r§   Úkeyr�   s        r   Ú_schedule_display_for_jobr­   ®  sÀ   € Ý˜sŸwšwÐ'9Ñ:Ô:Ñ;Ô;×AÒAÑCÔC€GØð Øˆà�wŠw�zÑ"Ô"€HÝ�(�DÑ!Ô!ð Ø9ð 	ð 	ˆCÝ# H§L¢L°Ñ$5Ô$5Ñ6Ô6×<Ò<Ñ>Ô>ˆDØð Ø���ðð	ð 
Ð	Ý�8‰}Œ}Ðàˆ3r/   c                 ó~  — t          | ¦  «        }t          |                     d¦  «        d¦  «        }t          |                     d¦  «        ¦  «        }||d<   ||d<   t          |                     d¦  «        ¦  «                             ¦   «         }|st          |                     d¦  «        ¦  «                             ¦   «         }|p*|                     d¦  «        r|d         d         ndp|p|pd	}|d
d…                              ¦   «         pd	}||d<   t	          |¦  «        |d<   t          |¦  «        |d<   |S )a,  Return a read-safe cron job shape for UI/API/tool/scheduler consumers.

    Older or hand-edited jobs can have nullable fields like ``prompt``,
    ``name``, or ``schedule_display``.  Keep storage untouched on read, but
    ensure consumers never crash while formatting or running those records.
    r‡   ÚunknownÚpromptÚnameÚscriptr“   r   rJ   úcron jobNé2   r¦   Ústate)rŸ   r¤   r4   rM   r­   Úeffective_job_state)rœ   r™   rX   r°   r±   r²   Úlabel_sources          r   Ú_normalize_job_recordr¸   ¿  sO  € õ % SÑ)Ô)€JÝ˜jŸnšn¨TÑ2Ô2°IÑ>Ô>€FÝ˜jŸnšn¨XÑ6Ô6Ñ7Ô7€FØ€JˆtÑØ!€JˆxÑå˜JŸNšN¨6Ñ2Ô2Ñ3Ô3×9Ò9Ñ;Ô;€DØð 	7Ý! *§.¢.°Ñ":Ô":Ñ;Ô;×AÒAÑCÔCˆàð Ø+5¯>ª>¸(Ñ+CÔ+CÐK�
˜8Ô$ QÔ'Ð'Èðàðð ðð ð 	ð ˜C˜R˜CÔ ×&Ò&Ñ(Ô(Ð6¨JˆØ€JˆvÑÝ%>¸zÑ%JÔ%J€JÐ!Ñ"õ
 .¨jÑ9Ô9€JˆwÑàÐr/   c                 óº   — t          |                      d¦  «        ¦  «                             ¦   «         dk    rdS t          |                      d¦  «        ¦  «        S )z>True when the record carries any operator-facing pause signal.rµ   ÚpausedTÚ	paused_at)r¤   r4   rM   r   ©rœ   s    r   Ú_has_pause_markerr½   â  sL   € å˜Ÿš Ñ(Ô(Ñ)Ô)×/Ò/Ñ1Ô1°XÒ=Ð=ØˆtÝ�—’˜Ñ$Ô$Ñ%Ô%Ð%r/   c                 óX   — |                       dd¦  «        sdS t          | ¦  «        rdS dS )zñTrue iff the scheduler may fire this job.

    ``enabled`` is the scheduler-honoured flag. Pause markers (``state`` /
    ``paused_at``) are a second gate so a contradictory half-paused record
    never fires even before self-heal runs.
    ÚenabledTF)r4   r½   r¼   s    r   Úis_job_runnablerÀ   é  s9   € ð �7Š7�9˜dÑ#Ô#ð ØˆuÝ˜ÑÔð ØˆuØˆ4r/   c                 ó  — t          |                      d¦  «        ¦  «                             ¦   «         }|dv r|S |                      dd¦  «        st          | ¦  «        s|dk    rdS |pdS |dk    s|                      d¦  «        rdS |pdS )u&  Operator-facing state derived from the scheduler-honoured flag.

    A job with ``enabled=true`` must never display as paused â€” that was the
    07-30 outage failure mode (list looked frozen, fleet kept merging).
    Terminal states (completed/error) are preserved regardless of enabled.
    rµ   >   r}   Ú	completedr¿   Trº   r»   Ú	scheduled)r¤   r4   rM   r½   )rœ   Ústoreds     r   r¶   r¶   ÷  s¦   € õ ˜cŸgšg gÑ.Ô.Ñ/Ô/×5Ò5Ñ7Ô7€FØÐ'Ð'Ð'ØˆØ�7Š7�9˜dÑ#Ô#ð "Ý˜SÑ!Ô!ð 	 V¨xÒ%7Ð%7Ø�8ØÐ!˜Ð!à�ÒÐ˜SŸWšW [Ñ1Ô1ÐØˆ{ØÐ �[Ð r/   Úpathc                 ób   — 	 t          j        | d¦  «         dS # t          t          f$ r Y dS w xY w)z<Set directory to owner-only access (0700). No-op on Windows.iÀ  N)rK   Úchmodr{   ÚNotImplementedError©rÅ   s    r   Ú_secure_dirrÊ     sG   € ðÝ
Œ��uÑÔÐÐÐøÝÕ(Ð)ð ð ð Øˆˆðøøøs   ‚ ™.­.c                 óŽ   — 	 |                       ¦   «         rt          j        | d¦  «         dS dS # t          t          f$ r Y dS w xY w)z;Set file to owner-only read/write (0600). No-op on Windows.i€  N)ÚexistsrK   rÇ   r{   rÈ   rÉ   s    r   Ú_secure_filerÍ     sa   € ðØ�;Š;‰=Œ=ð 	"ÝŒH�T˜5Ñ!Ô!Ð!Ð!Ð!ð	"ð 	"øåÕ(Ð)ð ð ð Øˆˆðøøøs   ‚)/ ¯AÁAÚbeforec                 ó´  — |�t           j        dk    rdS t          t           dd¦  «        }t          t           dd¦  «        }|�|€dS 	  |¦   «         }|dk    rdS |j        |j        f| |¦   «         fk    rdS t          j        | |j        |j        ¦  «         dS # t          $ r3}t                               d| |j        |j        |¦  «         Y d}~dS d}~ww xY w)u¥  Restore a rewritten file's previous owner (POSIX, privileged writer only).

    The atomic-write pattern (mkstemp + replace) makes the rewritten file owned
    by the *writer's* euid. When a root shell runs a state-writing cron CLI
    command (``docker exec hermes hermes cron create ...`` â€” ``docker exec``
    defaults to root) against a store owned by the unprivileged gateway user,
    the replace flips ``jobs.json`` to ``root:root`` mode 600 and the gateway's
    ticker (uid 1000) is silently locked out of every subsequent tick (#68483).

    Root can always hand ownership back, so do exactly that: when the euid is 0
    and the pre-replace owner differs, chown the new file to the previous
    uid/gid. Unprivileged writers are a no-op (their own rewrite already heals
    a root-owned file back to their uid, and they couldn't chown anyway).
    No-op on Windows. Best-effort: a failure must never break the save.
    NÚposixÚgeteuidÚgetegidr   u«   Could not restore ownership of %s to uid=%s gid=%s after rewrite: %s â€” if the gateway runs as a different user, its cron ticker may now be locked out (see issue #68483).)	rK   r±   rm   Úst_uidÚst_gidÚchownr{   r_   r`   )rÅ   rÎ   rÑ   rÒ   Úeuidr…   s         r   Ú_preserve_file_ownershipr×     s  € ð  €~�œ GÒ+Ð+ØˆÝ•b˜) TÑ*Ô*€GÝ•b˜) TÑ*Ô*€GØ€˜'˜/Øˆð
Øˆw‰yŒyˆØ�1Š9ˆ9ØˆFØŒM˜6œ=Ð)¨d°G°G±I´IÐ->Ò>Ð>ØˆFÝ
Œ��v”} f¤mÑ4Ô4Ð4Ð4Ð4øÝð 
ð 
ð 
Ý�Šð0ð �&”- ¤°ñ		
ô 	
ð 	
ð 	
ð 	
ð 	
ð 	
ð 	
ð 	
øøøøð
øøøs$   ÁB ÁB Á8 B Â
CÂ$(CÃCc                  óâ   — t          ¦   «         } | j                             dd¬¦  «         | j                             dd¬¦  «         t	          | j        ¦  «         t	          | j        ¦  «         dS )z6Ensure cron directories exist with secure permissions.T©ÚparentsÚexist_okN)r>   r'   Úmkdirr)   rÊ   ©Ústores    r   rq   rq   B  sk   € åÑ!Ô!€EØ	„N×Ò °ÐÑ5Ô5Ð5Ø	Ô×Ò 4°$ÐÑ7Ô7Ð7Ý�”ÑÔÐÝ�Ô Ñ!Ô!Ð!Ð!Ð!r/   Úsc                 ó>  — |                       ¦   «                              ¦   «         } t          j        d| ¦  «        }|st	          d| › d�¦  «        ‚t          |                     d¦  «        ¦  «        }|                     d¦  «        d         }dddd	œ}|||         z  S )
u€   
    Parse duration string into minutes.
    
    Examples:
        "30m" â†’ 30
        "2h" â†’ 120
        "1d" â†’ 1440
    zD^(\d+)\s*(m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)$zInvalid duration: 'z''. Use format like '30m', '2h', or '1d'rf   é   r   r!   i   )ÚmÚhÚd)rM   ÚlowerÚreÚmatchrP   ÚintÚgroup)rß   rç   r    ÚunitÚmultiplierss        r   Úparse_durationrì   O  s›   € ð 	
�Š‰	Œ	�ŠÑÔ€AÝŒHÐ\Ð^_Ñ`Ô`€EØð [ÝÐY¨qÐYÐYÐYÑZÔZÐZå�—’˜A‘”ÑÔ€EØ�;Š;�q‰>Œ>˜!Ô€Dà ¨Ð.Ð.€KØ�;˜tÔ$Ñ$Ð$r/   r§   c                 ó˜  — |                       ¦   «         } | }|                      ¦   «         }|                     d¦  «        r5| dd…                               ¦   «         }t          |¦  «        }d|d|› d�dœS |                      ¦   «         }t          |¦  «        dk    r|t          d„ |dd…         D ¦   «         ¦  «        r[t          ¦   «         st          d	¦  «        ‚	 t          | ¦  «         n'# t          $ r}t          d
| › d|› �¦  «        ‚d}~ww xY wd| | dœS d| v st          j        d| ¦  «        r®	 t          j        |                      dd¦  «        ¦  «        }|j        €)t#          ¦   «         j        }|                     |¬¦  «        }d|                     ¦   «         d|                     d¦  «        › �dœS # t          $ r}t          d| › d|› �¦  «        ‚d}~ww xY w	 t          | ¦  «        }t#          ¦   «         t)          |¬¦  «        z   }	d|	                     ¦   «         d|› �dœS # t          $ r Y nw xY wt          d|› d�¦  «        ‚)uM  
    Parse schedule string into structured format.
    
    Returns dict with:
        - kind: "once" | "interval" | "cron"
        - For "once": "run_at" (ISO timestamp)
        - For "interval": "minutes" (int)
        - For "cron": "expr" (cron expression)
    
    Examples:
        "30m"              â†’ once in 30 minutes
        "2h"               â†’ once in 2 hours
        "every 30m"        â†’ recurring every 30 minutes
        "every 2h"         â†’ recurring every 2 hours
        "0 9 * * *"        â†’ cron expression
        "2026-02-03T14:00" â†’ once at timestamp
    zevery é   NÚintervalrâ   )ÚkindÚminutesr¨   é   c              3   ó@   K  — | ]}t          j        d |¦  «        V — ŒdS )z^[\d\*\-,/]+$N)ræ   rç   )Ú.0Úps     r   ú	<genexpr>z!parse_schedule.<locals>.<genexpr>‡  s@   è è € ð ð Ø*+�ŒÐ! 1Ñ%Ô%ðð ð ð ð ð r/   zOCron expressions require 'croniter' package. Install with: pip install croniterzInvalid cron expression 'z': r   )rð   r©   r¨   ÚTz^\d{4}-\d{2}-\d{2}ÚZz+00:00©ÚtzinfoÚoncezonce at z%Y-%m-%d %H:%M)rð   rª   r¨   zInvalid timestamp '©rñ   zonce in zInvalid schedule 'zÄ'. Use:
  - Duration: '30m', '2h', '1d' (one-shot)
  - Interval: 'every 30m', 'every 2h' (recurring)
  - Cron: '0 9 * * *' (cron expression)
  - Timestamp: '2026-02-03T14:00:00' (one-shot at time))rM   rå   Ú
startswithrì   ÚsplitÚlenÚallr   rP   r   r^   ræ   rç   r   ÚfromisoformatÚreplacerú   Ú_hermes_nowÚ	isoformatÚstrftimer   )
r§   ÚoriginalÚschedule_lowerÚduration_strrñ   Úpartsr…   ÚdtÚ	hermes_tzrª   s
             r   Úparse_scheduler  d  sø  € ð$ �~Š~ÑÔ€HØ€HØ—^’^Ñ%Ô%€Nð × Ò  Ñ*Ô*ð 
Ø   ”|×)Ò)Ñ+Ô+ˆÝ  Ñ.Ô.ˆàØØ* Ð*Ð*Ð*ð
ð 
ð 	
ð �NŠNÑÔ€EÝ
ˆ5�z„z�Q‚€�3ð ð Ø/4°R°a°R¬yðñ ô ñ ô €õ  Ñ!Ô!ð 	pÝÐnÑoÔoÐoð	KÝ�XÑÔÐÐøÝð 	Kð 	Kð 	KÝÐI¸ÐIÐIÀaÐIÐIÑJÔJÐJøøøøð	Køøøð ØØð
ð 
ð 	
ð ˆh€€�"œ(Ð#8¸(ÑCÔC€ð	EåÔ'¨×(8Ò(8¸¸hÑ(GÔ(GÑHÔHˆBð ŒyÐ Ý'™MœMÔ0�	Ø—Z’Z y�ZÑ1Ô1�àØŸ,š,™.œ.ØE b§k¢kÐ2BÑ&CÔ&CÐEÐEðð ð øõ
 ð 	Eð 	Eð 	EÝÐC°8ÐCÐCÀÐCÐCÑDÔDÐDøøøøð	Eøøøð	Ý  Ñ*Ô*ˆÝ‘”¥°7Ð!;Ñ!;Ô!;Ñ;ˆàØ×&Ò&Ñ(Ô(Ø, (Ð,Ð,ð
ð 
ð 	
øõ
 ð ð ð Øˆðøøøõ ð	C˜Xð 	Cð 	Cð 	Cñô ð sD   ÃC+ Ã+
DÃ5D
Ä
DÄ2BF8 Æ8
GÇGÇGÇ AH) È)
H6È5H6r
  c                 ó  — t          ¦   «         j        }| j        €St          j        ¦   «                              ¦   «         j        }|                      |¬¦  «                             |¦  «        S |                      |¦  «        S )aÇ  Return a timezone-aware datetime in Hermes configured timezone.

    Backward compatibility:
    - Older stored timestamps may be naive.
    - Naive values are interpreted as *system-local wall time* (the timezone
      `datetime.now()` used when they were created), then converted to the
      configured Hermes timezone.

    This preserves relative ordering for legacy naive timestamps across
    timezone changes and avoids false not-due results.
    Nrù   )r  rú   r   r   Ú
astimezoner  )r
  Ú	target_tzÚlocal_tzs      r   Ú_ensure_awarer  È  sf   € õ ‘”Ô$€IØ	„yÐÝ”<‘>”>×,Ò,Ñ.Ô.Ô5ˆØ�zŠz ˆzÑ*Ô*×5Ò5°iÑ@Ô@Ð@Ø�=Š=˜Ñ#Ô#Ð#r/   rÄ   Úcurrentc                 óv   — | j         �|j         €dS |                      ¦   «         |                     ¦   «         k    S )u	  Return True when a stored aware timestamp uses a different UTC offset.

    Naive stored timestamps return False: they carry no offset to compare, and
    are normalized by ``_ensure_aware`` instead â€” they intentionally never take
    the offset-repair path.
    NF)rú   Ú	utcoffset©rÄ   r  s     r   Ú_timezone_offset_mismatchr  Û  s;   € ð „}Ð ¤Ð 6ØˆuØ×ÒÑÔ ×!2Ò!2Ñ!4Ô!4Ò4Ð4r/   c                 ó^   — |                       d¬¦  «        |                      d¬¦  «        k    S )aí  Return True when the stored local wall-clock time has not arrived yet.

    Cron schedules express local wall-clock intent. If Hermes/system local time
    changes after next_run_at was persisted, an old offset can make a future
    wall-clock run look due at the converted absolute time (for example
    21:00+10 becomes 13:00+02). Comparing naive wall-clock values lets us
    distinguish that migration case from a genuinely missed run whose scheduled
    wall time has already passed.
    Nrù   )r  r  s     r   Ú_stored_wall_clock_is_futurer  ç  s+   € ð �>Š> ˆ>Ñ&Ô&¨¯ªÀ¨Ñ)EÔ)EÒEÐEr/   ©Úlast_run_atr   r  c                óD  — t          | t          ¦  «        r|                      d¦  «        dk    rdS |rdS |                      d¦  «        }|sdS 	 t          t	          j        |¦  «        ¦  «        }n# t          $ r Y dS w xY w||t          t          ¬¦  «        z
  k    r|S dS )a  Return a one-shot run time if it is still eligible to fire.

    One-shot jobs get a small grace window so jobs created a few seconds after
    their requested minute still run on the next tick. Once a one-shot has
    already run, it is never eligible again.
    rð   rû   Nrª   )Úseconds)	r•   rž   r4   r  r   r  r^   r   ÚONESHOT_GRACE_SECONDS)r§   r   r  rª   Ú	run_at_dts        r   Ú_recoverable_oneshot_run_atr  ô  sÃ   € õ �h¥Ñ%Ô%ð ¨¯ª°fÑ)=Ô)=ÀÒ)GÐ)GØˆtØð Øˆtà�\Š\˜(Ñ#Ô#€FØð ØˆtðÝ!¥(Ô"8¸Ñ"@Ô"@ÑAÔAˆ	ˆ	øÝð ð ð Øˆtˆtðøøøà�C�)Õ,AÐBÑBÔBÑBÒBÐBØˆØˆ4s   Á!A1 Á1
A?Á>A?c                 óf  — d}d}|                       d¦  «        }|dk    r<|                       dd¦  «        dz  }|dz  }t          |t          ||¦  «        ¦  «        S |d	k    rÏt          ¦   «         rÁ|                       d
¦  «        }|rª	 t	          ¦   «         }t          ||¦  «        }|                     t          ¦  «        }	|                     t          ¦  «        }
t          |
|	z
   	                    ¦   «         ¦  «        }|dz  }t          |t          ||¦  «        ¦  «        S # t          $ r Y nw xY w|S )a(  Compute how late a job can be and still catch up instead of fast-forwarding.

    Uses half the schedule period, clamped between 120 seconds and 2 hours.
    This ensures daily jobs can catch up if missed by up to 2 hours,
    while frequent jobs (every 5-10 min) still fast-forward quickly.
    r#   i   rð   rï   rñ   rf   r!   rá   r   r©   )r4   rS   Úminr   r  r   Úget_nextr   rè   Útotal_secondsr^   )r§   Ú	MIN_GRACEÚ	MAX_GRACErð   Úperiod_secondsÚgracer©   r   r   ÚfirstÚseconds              r   Ú_compute_grace_secondsr*    s;  € ð €IØ€Ià�<Š<˜ÑÔ€DàˆzÒÐØ!Ÿš i°Ñ3Ô3°bÑ8ˆØ !Ñ#ˆÝ�9�c %¨Ñ3Ô3Ñ4Ô4Ð4àˆv‚~€~Õ*Ñ,Ô,€~Ø�|Š|˜FÑ#Ô#ˆØð 
	ð	Ý!‘m”m�Ý  cÑ*Ô*�ØŸš¥hÑ/Ô/�ØŸš¥xÑ0Ô0�Ý!$ f¨u¡n×%CÒ%CÑ%EÔ%EÑ!FÔ!F�Ø&¨!Ñ+�Ý˜9¥c¨%°Ñ&;Ô&;Ñ<Ô<Ð<øÝð ð ð Ø�ðøøøð Ðs   ÂBD! Ä!
D.Ä-D.c                 óv  — t          ¦   «         }t          | t          ¦  «        sdS |                      d¦  «        }|€dS |dk    rt	          | ||¬¦  «        S |dk    r›|                      d¦  «        }|€dS |rY	 t          t          j        |¦  «        ¦  «        }|t          |¬¦  «        z   }n6# t          $ r |t          |¬¦  «        z   }Y nw xY w|t          |¬¦  «        z   }| 
                    ¦   «         S |dk    r»|                      d	¦  «        }|sdS t          ¦   «         st                               d
|¦  «         dS |}|r5	 t          t          j        |¦  «        ¦  «        }n# t          $ r |}Y nw xY wt          ||¦  «        }	|	                     t          ¦  «        }| 
                    ¦   «         S dS )zo
    Compute the next run time for a schedule.

    Returns ISO timestamp string, or None if no more runs.
    Nrð   rû   r  rï   rñ   rü   r   r©   zÀCannot compute next run for cron schedule %r: 'croniter' is not installed. croniter is a core dependency as of v0.9.x; reinstall hermes-agent or run 'pip install croniter' in your runtime env.)r  r•   rž   r4   r  r  r   r  r   r^   r  r   r_   r`   r   r"  )
r§   r  r   rð   rñ   ÚlastÚnext_runr©   Ú	base_timer   s
             r   Úcompute_next_runr/  4  s  € õ ‰-Œ-€Cå�h¥Ñ%Ô%ð ØˆtØ�<Š<˜ÑÔ€DØ€|Øˆtàˆv‚~€~Ý*¨8°SÀkÐRÑRÔRÐRà	�Ò	Ð	Ø—,’,˜yÑ)Ô)ˆØˆ?Ø�4Øð 	8ð<Ý$¥XÔ%;¸KÑ%HÔ%HÑIÔI�Ø¥)°GÐ"<Ñ"<Ô"<Ñ<��øÝð <ð <ð <Ø¥°7Ð!;Ñ!;Ô!;Ñ;���ð<øøøð �Y¨wÐ7Ñ7Ô7Ñ7ˆHØ×!Ò!Ñ#Ô#Ð#à	�ŠˆØ�|Š|˜FÑ#Ô#ˆØð 	Ø�4ÝÑ!Ô!ð 	Ý�NŠNðð ñô ð ð �4ð
 ˆ	Øð 	 ð Ý)­(Ô*@ÀÑ*MÔ*MÑNÔN�	�	øÝð  ð  ð  Ø�	�	�	ð øøøå˜˜iÑ(Ô(ˆØ—=’=¥Ñ*Ô*ˆØ×!Ò!Ñ#Ô#Ð#àˆ4s$   Á94B. Â.CÃCÅ!E) Å)E8Å7E8c                 ó‚   — t          ¦   «          t          | t          t          j        ¦   «         ¦  «        d¬¦  «         dS )aR  Atomically write the current epoch time to ``path``.

    Delegates to :func:`utils.atomic_write_text` (tmpfile + fsync +
    ``atomic_replace``, same pattern as ``save_jobs``) so a concurrent reader
    in another process (``hermes cron status``) never sees a torn/truncated
    file. Best-effort: failures are swallowed by callers.
    z.hb_)Ú
tmp_prefixN)rq   r   r�   ru   rÉ   s    r   Ú_atomic_write_epochr2  v  s6   € õ �M„M€MÝ�d�C¥¤	¡¤Ñ,Ô,¸Ð@Ñ@Ô@Ð@Ð@Ð@r/   c           	      ó8  — t          ¦   «          t          j        t          | j        ¦  «        dd¬¦  «        \  }}	 t          j        |dd¬¦  «        5 }|                     t          t          d|¦  «        ¦  «        ¦  «         | 	                    ¦   «          t          j
        |                     ¦   «         ¦  «         ddd¦  «         n# 1 swxY w Y   t          || ¦  «         dS # t          $ r( 	 t          j        |¦  «         n# t          $ r Y nw xY w‚ w xY w)	z2Atomically persist a non-negative integer counter.ú.tmpz.count_©ÚdirÚsuffixÚprefixÚwrg   rh   r   N)rq   ÚtempfileÚmkstempr�   ÚparentrK   ÚfdopenÚwriterS   ÚflushÚfsyncr�   r   ÚBaseExceptionÚunlinkr{   )rÅ   r    ÚfdÚtmp_pathÚfs        r   Ú_atomic_write_counterrF  ‚  sT  € å�M„M€MÝÔ#­¨D¬KÑ(8Ô(8ÀÐPYÐZÑZÔZ�L€BˆðÝŒY�r˜3¨Ð1Ñ1Ô1ð 	!°QØ�GŠG•C�˜A˜u™œÑ&Ô&Ñ'Ô'Ð'Ø�GŠG‰IŒIˆIÝŒH�Q—X’X‘Z”ZÑ Ô Ð ð	!ð 	!ð 	!ñ 	!ô 	!ð 	!ð 	!ð 	!ð 	!ð 	!ð 	!øøøð 	!ð 	!ð 	!ð 	!õ 	�x Ñ&Ô&Ð&Ð&Ð&øÝð ð ð ð	ÝŒI�hÑÔÐÐøÝð 	ð 	ð 	ØˆDð	øøøàðøøøsT   ¼C' ÁA+C
Â>C' Ã
CÃC' ÃCÃC' Ã'
DÃ2DÄDÄ
DÄDÄDÄDFÚsuccessc                 óÎ   — t          ¦   «         }	 t          |j        dz  ¦  «         n# t          $ r Y nw xY w| r+	 t          |j        dz  ¦  «         dS # t          $ r Y dS w xY wdS )u?  Record a ticker liveness signal, and optionally a successful-tick signal.

    The ticker calls this once per loop iteration. ``success=True`` additionally
    bumps the *last successful tick* marker. We track two distinct signals so
    `hermes cron status` can tell a thread that is merely *alive and looping*
    (heartbeat fresh, success stale) from one that is actually *firing jobs*
    (both fresh) â€” a ticker stuck failing every tick would otherwise keep the
    plain heartbeat fresh and falsely report healthy (#32612, #32895).

    Resolution uses ``_current_cron_store()`` so the heartbeat is correctly
    scoped to the active profile's store â€” critical under multiplex_profiles
    where each profile needs its own liveness signal (#69377).

    Best-effort: a write failure must never disrupt the tick loop.
    r   r    N)r>   r2  r'   r^   )rG  rÞ   s     r   Úrecord_ticker_heartbeatrI  ”  s¢   € õ   Ñ!Ô!€EðÝ˜EœNÐ-?Ñ?Ñ@Ô@Ð@Ð@øÝð ð ð Øˆðøøøàð ð	Ý ¤Ð1FÑ FÑGÔGÐGÐGÐGøÝð 	ð 	ð 	ØˆDˆDð	øøøðð s   �( ¨
5´5»A Á
A"Á!A"c                 óØ   — 	 |                       d¬¦  «                             ¦   «         }t          dt          j        ¦   «         t	          |¦  «        z
  ¦  «        S # t
          $ r Y d S w xY w)Nrg   rh   g        )Ú	read_textrM   rS   ru   rO   r^   )rÅ   rU   s     r   Ú_epoch_file_agerL  °  si   € ðØ�nŠn gˆnÑ.Ô.×4Ò4Ñ6Ô6ˆÝ�3�œ	™œ¥e¨C¡j¤jÑ0Ñ1Ô1Ð1øÝð ð ð Øˆtˆtðøøøs   ‚AA Á
A)Á(A)c                  óL   — t          ¦   «         } t          | j        dz  ¦  «        S )u·  Seconds since the ticker loop last iterated, or None if unknown.

    None = heartbeat file missing/unreadable (older build, never ran, or a
    torn read). Callers treat None as "cannot determine", not "dead".

    Resolution uses ``_current_cron_store()`` so the heartbeat is correctly
    scoped to the active profile â€” critical under multiplex_profiles where
    ``hermes cron status`` must report per-profile liveness (#69377).
    r   ©r>   rL  r'   rÝ   s    r   Úget_ticker_heartbeat_agerO  ¸  s%   € õ  Ñ!Ô!€EÝ˜5œ>Ð,>Ñ>Ñ?Ô?Ð?r/   c                  óL   — t          ¦   «         } t          | j        dz  ¦  «        S )u-  Seconds since the ticker last completed a tick WITHOUT raising, or None.

    Resolution uses ``_current_cron_store()`` so the heartbeat is correctly
    scoped to the active profile â€” critical under multiplex_profiles where
    ``hermes cron status`` must report per-profile liveness (#69377).
    r    rN  rÝ   s    r   Úget_ticker_success_agerQ  Æ  s%   € õ  Ñ!Ô!€EÝ˜5œ>Ð,AÑAÑBÔBÐBr/   c                  ó8  — t          ¦   «         j        dz  } 	 	 t          |                      d¬¦  «                             ¦   «         ¦  «        }n# t
          t          f$ r d}Y nw xY wt          | t          d|¦  «        dz   ¦  «         dS # t          $ r Y dS w xY w)zIIncrement the profile-local stale-schedule catch-up counter, best effort.Úcatch_up_occurrencesrg   rh   r   rf   N)
r>   r'   rè   rK  rM   r{   rP   rF  rS   r^   )rÅ   r    s     r   Úrecord_catch_up_occurrencerT  Ñ  s·   € åÑ Ô Ô)Ð,BÑB€Dðð	Ý˜Ÿš°˜Ñ8Ô8×>Ò>Ñ@Ô@ÑAÔAˆEˆEøÝ�Ð$ð 	ð 	ð 	ØˆEˆEˆEð	øøøå˜d¥C¨¨5¡M¤M°AÑ$5Ñ6Ô6Ð6Ð6Ð6øÝð ð ð Øˆˆðøøøs/   ™5A ÁB ÁA%Á"B Á$A%Á%$B Â
BÂBÚmessagec                 ó¨  — t          ¦   «         }|j        dz  }	 t          ¦   «          t          j        t          |j        ¦  «        dd¬¦  «        \  }}	 t          j        |dd¬¦  «        5 }| 	                    t          j
        ¦   «         › d|                      ¦   «         › d�¦  «         |                     ¦   «          t          j        |                     ¦   «         ¦  «         d	d	d	¦  «         n# 1 swxY w Y   t          ||¦  «         d	S # t           $ r( 	 t          j        |¦  «         n# t$          $ r Y nw xY w‚ w xY w# t&          $ r Y d	S w xY w)
aO  Persist the most recent tick failure so other processes can surface it.

    The ticker thread lives inside the gateway process; ``hermes cron
    status``/``list`` run in a separate process and previously could only
    infer "ticks may be failing" from marker staleness, with no clue WHY.
    A root-owned ``jobs.json`` (#68483) failed every tick for ~14h with the
    reason visible only in the gateway's errors.log. Writing the last error
    next to the heartbeat markers gives the CLI something concrete to show.

    Best-effort: a write failure must never disrupt the tick loop.
    Úticker_last_errorr4  z.terr_r5  r9  rg   rh   Ú
N)r>   r'   rq   r:  r;  r�   r<  rK   r=  r>  ru   rM   r?  r@  r�   r   rA  rB  r{   r^   )rU  rÞ   rÅ   rC  rD  rE  s         r   Úrecord_ticker_errorrY  Þ  s¬  € õ  Ñ!Ô!€EØŒ>Ð/Ñ/€DðÝ‰ŒˆÝÔ'Ý�D”KÑ Ô ¨¸ð
ñ 
ô 
‰ˆˆHð	Ý”˜2˜s¨WÐ5Ñ5Ô5ð %¸Ø—’�4œ9™;œ;Ð=Ð=¨'¯-ª-©/¬/Ð=Ð=Ð=Ñ>Ô>Ð>Ø—’‘	”	�	Ý”˜Ÿš™œÑ$Ô$Ð$ð%ð %ð %ñ %ô %ð %ð %ð %ð %ð %ð %øøøð %ð %ð %ð %õ ˜8 TÑ*Ô*Ð*Ð*Ð*øÝð 	ð 	ð 	ðÝ”	˜(Ñ#Ô#Ð#Ð#øÝð ð ð Ø�ðøøøàð	øøøøõ ð ð ð Øˆˆðøøøsl   š:E ÁD Á,A9C1Ã%D Ã1C5Ã5D Ã8C5Ã9D Ä
E ÄD.Ä-E Ä.
D;Ä8E Ä:D;Ä;E Å E Å
EÅEc                  óæ   — t          ¦   «         j        dz  } 	 t          dt          |                      d¬¦  «                             ¦   «         ¦  «        ¦  «        S # t          t          f$ r Y dS w xY w)z7Return the profile-local stale-schedule catch-up count.rS  r   rg   rh   )r>   r'   rS   rè   rK  rM   r{   rP   rÉ   s    r   Úget_catch_up_occurrence_countr[    st   € åÑ Ô Ô)Ð,BÑB€DðÝ�1•c˜$Ÿ.š.°'˜.Ñ:Ô:×@Ò@ÑBÔBÑCÔCÑDÔDÐDøÝ•ZÐ ð ð ð Øˆqˆqðøøøs   ˜AA ÁA0Á/A0c                  ó~   — t          ¦   «         } 	 | j        dz                       ¦   «          dS # t          $ r Y dS w xY w)zGRemove the last-tick-error marker after a successful tick. Best-effort.rW  N)r>   r'   rB  r{   rÝ   s    r   Úclear_ticker_errorr]  
  sT   € åÑ!Ô!€EðØ	ŒÐ-Ñ	-×5Ò5Ñ7Ô7Ð7Ð7Ð7øÝð ð ð Øˆˆðøøøs   �. ®
<»<c                  ó8  — t          ¦   «         } 	 | j        dz                       d¬¦  «        }n# t          $ r Y dS w xY w|                     ¦   «         }t          |¦  «        dk     rdS d                     |dd…         ¦  «                             ¦   «         }|pdS )z<Return the most recent recorded tick error message, or None.rW  rg   rh   Nrá   rX  rf   )r>   r'   rK  r^   Ú
splitlinesrÿ   ÚjoinrM   )rÞ   rU   ÚlinesrU  s       r   Úget_ticker_last_errorrb    s¦   € åÑ!Ô!€EðØŒ~Ð 3Ñ3×>Ò>ÈÐ>ÑPÔPˆˆøÝð ð ð Øˆtˆtðøøøà�NŠNÑÔ€EÝ
ˆ5�z„z�A‚~€~ØˆtØ�iŠi˜˜a˜b˜bœ	Ñ"Ô"×(Ò(Ñ*Ô*€GØˆ?�dÐs   �/ ¯
=¼=r(   c                 ó  — t          | dd¬¦  «        5 }|                     ¦   «         }ddd¦  «         n# 1 swxY w Y   	 t          j        |¦  «        dfS # t          j        $ r t          j        |d¬¦  «        dfcY S w xY w)a«  Tolerantly parse jobs.json; shared by load_jobs and the save-path peek.

    Returns ``(data, used_strict_fallback)``. utf-8-sig absorbs a Windows
    BOM; a strict parse failure is retried with ``strict=False`` to survive
    bare control characters in string values. IO errors from the open and
    parse errors from the fallback propagate to the caller, which decides
    between repair (load_jobs) and bail-out (peek).
    Úrz	utf-8-sigrh   NF)ÚstrictT)rr   ÚreadÚjsonÚloadsÚJSONDecodeError)r(   rE  rU   s      r   Ú_parse_jobs_filerj  %  sÎ   € õ 
ˆi˜ {Ð	3Ñ	3Ô	3ð °qØ�fŠf‰hŒhˆðð ð ñ ô ð ð ð ð ð ð øøøð ð ð ð ð3ÝŒz˜#‰Œ Ð%Ð%øÝÔð 3ð 3ð 3ÝŒz˜# eÐ,Ñ,Ô,¨dÐ2Ð2Ð2Ð2ð3øøøs   “4´8»8Á A Á'B Á?B c                  ó†  — t          ¦   «         j        } t          ¦   «          t          | ¦  «        }|                      ¦   «         st          d¦  «         g S 	 t          | ¦  «        \  }}n{# t          $ r3}t           	                    d|¦  «         t          d|› �¦  «        |‚d}~wt          $ r3}t           	                    d|¦  «         t          d|› �¦  «        |‚d}~ww xY wt          |t          ¦  «        rT|                     dg ¦  «        }|r+|r)t          |¦  «         t                               d¦  «         t          |¦  «         |S t          |t"          ¦  «        r<|r)t          |¦  «         t                               d¦  «         t          |¦  «         |S t          d	t%          |¦  «        j        › �¦  «        ‚)
zLoad all jobs from storage.NzIOError reading jobs.json: %szFailed to read cron database: z#Failed to auto-repair jobs.json: %sz*Cron database corrupted and unrepairable: Újobsz8Auto-repaired jobs.json (had invalid control characters)z3Auto-repaired jobs.json (bare list wrapped as dict)z7Cron database corrupted: expected {'jobs': [...]}, got )r>   r(   rq   Ú_jobs_file_stamprÌ   Ú_record_load_stamprj  r|   r_   r}   ÚRuntimeErrorr^   r•   rž   r4   Ú	save_jobsr`   r–   Útyper*   )r(   Úpre_read_stampÚdataÚ_strict_retryr…   rl  s         r   Ú	load_jobsru  6  sß  € å#Ñ%Ô%Ô/€IÝ�M„M€Mõ & iÑ0Ô0€NØ×ÒÑÔð Ý˜4Ñ Ô Ð Øˆ	ðTÝ.¨yÑ9Ô9ÑˆˆmˆmøÝð Hð Hð HÝ�ŠÐ4°aÑ8Ô8Ð8ÝÐ?¸AÐ?Ð?Ñ@Ô@ÀaÐGøøøøÝð Tð Tð TÝ�ŠÐ:¸AÑ>Ô>Ð>ÝÐKÈÐKÐKÑLÔLÐRSÐSøøøøðTøøøõ �$�ÑÔð Ø�xŠx˜ Ñ#Ô#ˆØð 	W˜Tð 	Wå�d‰OŒOˆOÝ�NŠNÐUÑVÔVÐVÝ˜>Ñ*Ô*Ð*ØˆÝ�$�ÑÔð ð ð 	RÝ�d‰OŒOˆOÝ�NŠNÐPÑQÔQÐQÝ˜>Ñ*Ô*Ð*Øˆå
ØYÅDÈÁJÄJÔDWÐYÐYñô ð s$   ÁA* Á*
C"Á4.B"Â"C"Â/.CÃC"c                  óX  — t          ¦   «         j        } |                      ¦   «         sg S 	 t          | ¦  «        \  }}n# t          $ r Y dS w xY wt          |t          ¦  «        r/|                     dg ¦  «        }t          |t          ¦  «        r|ndS t          |t          ¦  «        r|S dS )uÊ  Best-effort read of on-disk jobs without repair side-effects.

    Caller must hold ``_jobs_lock()``. Returns ``[]`` when the file is
    missing, ``None`` when the payload is unreadable/corrupt (caller should
    not attempt a shrink-merge against an unknown baseline). Never calls
    ``save_jobs`` â€” the repair-free property is what keeps the save path
    re-entrancy-safe (a repairing read here would recurse through
    ``_save_jobs_unlocked``).
    Nrl  )	r>   r(   rÌ   rj  r^   r•   rž   r4   r–   )r(   rs  Ú_rl  s       r   Ú_peek_jobs_unlockedrx  e  s¿   € õ $Ñ%Ô%Ô/€IØ×ÒÑÔð Øˆ	ðÝ" 9Ñ-Ô-‰ˆˆaˆaøÝð ð ð Øˆtˆtðøøøå�$�ÑÔð 8Ø�xŠx˜ Ñ#Ô#ˆÝ! $­Ñ-Ô-Ð7ˆtˆt°4Ð7Ý�$�ÑÔð ØˆØˆ4s   «> ¾
AÁAc                 óv   — 	 |                       ¦   «         }|j        |j        |j        fS # t          $ r Y dS w xY w)u^  Cheap change-detection stamp for jobs.json: ``(mtime_ns, size, ino)``.

    ``None`` means the file is missing/unstatable. Used as a fast-path gate
    in front of the shrink-merge so the healthy no-race save costs one
    ``stat()`` instead of a full read+parse (the ``advance_next_runs``
    batching exists because this path is hot â€” see its docstring).
    ``st_ino`` is included because every legitimate writer goes through
    mkstemp+rename (new inode), so even a same-size write inside one mtime
    quantum on a coarse-clock filesystem (ext4 jiffies, network mounts)
    cannot false-match.
    N)ÚstatÚst_mtime_nsÚst_sizeÚst_inor{   )r(   Ústs     r   rm  rm  ~  sK   € ðØ�^Š^ÑÔˆØ” ¤
¨B¬IÐ6Ð6øÝð ð ð Øˆtˆtðøøøs   ‚'* ª
8·8Ústampc                 óN   — t          t          dd¦  «        sdS | t          _        dS )ue  Remember jobs.json's stamp for the enclosing _jobs_lock() section.

    No-op outside a critical section. Lets the save path skip the
    shrink-merge parse when the file provably hasn't changed since this
    section loaded it (#80703's fast-path). The caller must capture the
    stamp BEFORE reading the file: a sibling landing mid-read then leaves
    the recorded stamp OLDER than disk â€” a mismatch, so the merge runs
    (fail-safe direction). Stamping after the read would let that sibling's
    write be certified as "seen" without being in the loaded payload,
    wrongly suppressing the recovery.
    re   r   N)rm   rn   rp   )r  s    r   rn  rn  ‘  s-   € õ Õ# W¨aÑ0Ô0ð ØˆØ"'ÕÔÐÐr/   ©Úremoved_idsrl  r‚  c                óî  — t          t          dd¦  «        }|�&t          t          ¦   «         j        ¦  «        |k    r| S t          ¦   «         }|€| S d„ |pdD ¦   «         }t          ¦   «         }| D ]T}t          |t          ¦  «        r=| 	                    d¦  «        r(| 
                    t          |d         ¦  «        ¦  «         ŒUg }|D ]r}t          |t          ¦  «        sŒ| 	                    d¦  «        }	|	sŒ0t          |	¦  «        }	|	|v s|	|v rŒH|                     |¦  «         | 
                    |	¦  «         Œs|s| S t                               dt          |¦  «        d„ |D ¦   «         ¦  «         | |z   S )u  Return *jobs* plus any on-disk jobs missing from the save payload (#80624).

    Under ``_jobs_lock()``'s degraded flock-timeout path (#60703), two
    processes can both believe they own the store. A writer that loaded an
    older/smaller snapshot then calls ``save_jobs`` and would otherwise
    clobber concurrent creates (the filed ``no_agent`` watchdog pattern:
    CLI/tool create succeeds, then a gateway tick/remove rewrites
    ``jobs.json`` empty or without the new id).

    Intentional deletes pass ``removed_ids``. Any other id present on disk
    but absent from *jobs* is treated as a concurrent create and merged
    back before the atomic write. The caller's list is never mutated â€” a
    new list is returned when anything was recovered.

    Fast path: when the enclosing critical section recorded a load stamp
    and the file's ``(mtime_ns, size)`` still matches, nothing can have
    changed underneath us, so the read+parse is skipped entirely â€” one
    ``stat()`` on the healthy no-race save.
    rp   Nc                 ó0   — h | ]}|¯t          |¦  «        ’ŒS r.   r£   ©rô   Úis     r   ú	<setcomp>z._merge_unexpected_disk_jobs.<locals>.<setcomp>Â  s#   € Ð@Ð@Ð@ !¸aÐ@•s˜1‘v”vÐ@Ð@Ð@r/   r.   r‡   z™Preserved %d cron job(s) present on disk but missing from the in-memory save payload (concurrent create under degraded lock or stale writer) (#80624): %sc                 ó8   — g | ]}|                      d ¦  «        ‘ŒS ©r‡   ©r4   ©rô   Újs     r   ú
<listcomp>z/_merge_unexpected_disk_jobs.<locals>.<listcomp>Ü  s"   € Ð(Ð(Ð(˜ˆ�Šˆt‰ŒÐ(Ð(Ð(r/   )rm   rn   rm  r>   r(   rx  rA   r•   rž   r4   Úaddr�   r—   r_   r`   rÿ   )
rl  r‚  r  Ú	disk_jobsÚintended_removeÚnew_idsrœ   Ú	recoveredÚdisk_jobÚdisk_ids
             r   Ú_merge_unexpected_disk_jobsr•  ¢  s§  € õ0 Õ$ l°DÑ9Ô9€EØÐÕ-Õ.AÑ.CÔ.CÔ.MÑNÔNÐRWÒWÐWØˆå#Ñ%Ô%€IØÐØˆà@Ð@¨Ð(9°rÐ@Ñ@Ô@€OÝ™œ€GØð (ð (ˆÝ�c�4Ñ Ô ð 	( S§W¢W¨T¡]¤]ð 	(Ø�KŠK�˜C œI™œÑ'Ô'Ð'øà&(€IØð 
ð 
ˆÝ˜(¥DÑ)Ô)ð 	ØØ—,’,˜tÑ$Ô$ˆØð 	ØÝ�g‘,”,ˆØ�gÐÐ ¨OÐ!;Ð!;ØØ×Ò˜Ñ"Ô"Ð"Ø�Š�GÑÔÐÐàð ØˆÝ
‡N‚Nð	(õ 	ˆI‰ŒØ(Ð(˜iÐ(Ñ(Ô(ñô ð ð �)ÑÐr/   ©r‚  r  r  c                ó®  ‡‡— t          ¦   «         j        }t          ¦   «          	 t          j        |¦  «        }n=# t
          $ r0 	 t          j        |j        ¦  «        }n# t
          $ r d}Y nw xY wY nw xY wd}	 t          d¦  «        D �]}|st          | |¬¦  «        } t          j
        t          |j        ¦  «        dd¬¦  «        \  }}	 t          j        |dd¬	¦  «        5 }t          j        | t          ¦   «                              ¦   «         d
œ|dd¬¦  «         |                     ¦   «          t          j        |                     ¦   «         ¦  «         ddd¦  «         n# 1 swxY w Y   n7# t(          $ r* 	 t          j        |¦  «         n# t
          $ r Y nw xY wd}‚ w xY w|s¡t-          t.          dd¦  «        }	|	duot1          |¦  «        |	k    }
|
rdnt3          ¦   «         }|�`d„ | D ¦   «         Šd„ |pdD ¦   «         Št5          ˆˆfd„|D ¦   «         ¦  «        r*	 t          j        |¦  «         n# t
          $ r Y nw xY wd}�ŒÀt7          ||¦  «         d}t9          |¦  «         t;          ||¦  «         t=          d¦  «          dS |st          | |¬¦  «        } t          j
        t          |j        ¦  «        dd¬¦  «        \  }}t          j        |dd¬	¦  «        5 }t          j        | t          ¦   «                              ¦   «         d
œ|dd¬¦  «         |                     ¦   «          t          j        |                     ¦   «         ¦  «         ddd¦  «         n# 1 swxY w Y   t7          ||¦  «         d}t9          |¦  «         t;          ||¦  «         dS # t(          $ r* |�&	 t          j        |¦  «         n# t
          $ r Y nw xY w‚ w xY w)a  Save all jobs to storage. Caller must hold _jobs_lock().

    ``removed_ids`` lists job ids this mutation intentionally deleted.
    ``replace=True`` skips the shrink-merge guard (tests / disaster recovery
    that mean to rewrite the store wholesale).
    Nrò   r�  r4  z.jobs_r5  r9  rg   rh   )rl  Ú
updated_atrá   F)ÚindentÚensure_asciirp   c                 óŒ   — h | ]A}t          |t          ¦  «        ¯|                     d ¦  «        ¯,t          |d          ¦  «        ’ŒBS r‰  ©r•   rž   r4   r�   r‹  s     r   r‡  z&_save_jobs_unlocked.<locals>.<setcomp>(  sV   € ð #ð #ð #àÝ% a­Ñ.Ô.ð#ð 45·5²5¸±;´;ð#Ý˜A˜dœG™œð#ð #ð #r/   c                 ó0   — h | ]}|¯t          |¦  «        ’ŒS r.   r£   r…  s     r   r‡  z&_save_jobs_unlocked.<locals>.<setcomp>-  s#   € ÐIÐIÐI¨1ÀqÐI¥ A¡¤ÐIÐIÐIr/   r.   c              3   óÈ   •K  — | ]\}t          |t          ¦  «        oB|                     d ¦  «        o-t          |d          ¦  «        ‰vot          |d          ¦  «        ‰vV — Œ]dS ©r‡   Nrœ  )rô   ÚdjÚintendedÚpayload_idss     €€r   rö   z&_save_jobs_unlocked.<locals>.<genexpr>.  s…   øè è € ð ð ð
 õ	 # 2¥tÑ,Ô,ð :ØŸFšF 4™LœLð:å  4¤™MœM°Ð<ð:õ    4¤™MœM°Ð9ð	ð ð ð ð ð r/   )r>   r(   rq   rK   rz  r{   r<  Úranger•  r:  r;  r�   r=  rg  Údumpr  r  r?  r@  r�   rA  rB  rm   rn   rm  rx  Úanyr   rÍ   r×   rn  )rl  r‚  r  r(   Ú_stat_beforerD  Ú_attemptrC  rE  Ú_stampÚ
_unchangedr�  r¡  r¢  s               @@r   Ú_save_jobs_unlockedrª  á  s6  øø€ õ $Ñ%Ô%Ô/€IÝ�M„M€Mð Ý”w˜yÑ)Ô)ˆˆøÝð  ð  ð  ð	 Ýœ7 9Ô#3Ñ4Ô4ˆLˆLøÝð 	 ð 	 ð 	 ØˆLˆLˆLð	 øøøøøð øøøð €Hð`Ý˜a™œð D	ñ D	ˆHØð RÝ2°4À[ÐQÑQÔQ�Ý#Ô+Ý˜	Ô(Ñ)Ô)°&Àðñ ô ‰LˆB�ðÝ”Y˜r 3°Ð9Ñ9Ô9ð )¸QÝ”IØ!%µ[±]´]×5LÒ5LÑ5NÔ5NÐOÐOØØ Ø%*ð	ñ ô ð ð —G’G‘I”I�IÝ”H˜QŸXšX™ZœZÑ(Ô(Ð(ð)ð )ð )ñ )ô )ð )ð )ð )ð )ð )ð )øøøð )ð )ð )ð )øøõ !ð ð ð ðÝ”I˜hÑ'Ô'Ð'Ð'øÝð ð ð Ø�Dðøøøà�Øðøøøð ð !õ
 !Õ!1°<ÀÑFÔF�à $Ð&ÐPÕ+;¸IÑ+FÔ+FÈ&Ò+Pð ð %/ÐI˜D˜DÕ4GÑ4IÔ4I�	ØÐ(ð#ð #à!%ð#ñ #ô #�Kð
  JÐI°Ð1BÀÐIÑIÔI�HÝð ð ð ð ð ð
 #,ðñ ô ñ ô ð !ð!ÝœI hÑ/Ô/Ð/Ð/øÝ&ð !ð !ð !Ø ˜Dð!øøøà#'˜Ù å˜8 YÑ/Ô/Ð/ØˆHÝ˜Ñ#Ô#Ð#Ý$ Y°Ñ=Ô=Ð=õ ˜tÑ$Ô$Ð$ØˆFˆFð ð 	NÝ.¨tÀÐMÑMÔMˆDÝÔ'Ý�IÔ$Ñ%Ô%¨f¸Xð
ñ 
ô 
‰ˆˆHõ ŒY�r˜3¨Ð1Ñ1Ô1ð 	!°QÝŒIØ­[©]¬]×-DÒ-DÑ-FÔ-FÐGÐGØØØ"ð	ñ ô ð ð �GŠG‰IŒIˆIÝŒH�Q—X’X‘Z”ZÑ Ô Ð ð	!ð 	!ð 	!ñ 	!ô 	!ð 	!ð 	!ð 	!ð 	!ð 	!ð 	!øøøð 	!ð 	!ð 	!ð 	!õ 	�x Ñ+Ô+Ð+ØˆÝ�YÑÔÐÝ  ¨LÑ9Ô9Ð9Ð9Ð9øÝð ð ð ØÐðÝ”	˜(Ñ#Ô#Ð#Ð#øÝð ð ð Ø�ðøøøàðøøøs  ¥: º
A4ÁAÁA4ÁA.Á+A4Á-A.Á.A4Á3A4Á:AN  ÃE/Ã#A4E#ÅE/Å#E'	Å'E/Å*E'	Å+E/Å.N  Å/
F#Å:FÆF#Æ
FÆF#ÆFÆF#Æ#A<N  È H5È4N  È5
IÈ?N  ÉIÉAN  ÊAN  Ë"A4M"ÍN  Í"M&Í&N  Í)M&Í*4N  Î OÎ-OÏOÏ
OÏOÏOÏOc                óx   — t          ¦   «         5  t          | ||¬¦  «         ddd¦  «         dS # 1 swxY w Y   dS )z¯Save all jobs to storage.

    See ``_save_jobs_unlocked`` for ``removed_ids`` / ``replace`` semantics
    (shrink-merge guard against concurrent-create clobber, #80624).
    r–  N)r†   rª  )rl  r‚  r  s      r   rp  rp  g  s›   € õ 
‰Œð Lð LÝ˜D¨kÀ7ÐKÑKÔKÐKðLð Lð Lñ Lô Lð Lð Lð Lð Lð Lð Lð Løøøð Lð Lð Lð Lð Lð Ls   �/¯3¶3Úworkdirc                 óÂ  — | €dS t          | ¦  «                             ¦   «         }|sdS t          |¦  «                             ¦   «         }|                     ¦   «         st          d|›d�¦  «        ‚|                     ¦   «         }|                     ¦   «         st          d|› �¦  «        ‚|                     ¦   «         st          d|› �¦  «        ‚t          |¦  «        S )uh  Normalize and validate a cron job workdir.

    Rules:
      - Empty / None â†’ None (feature off, preserves old behaviour).
      - ``~`` is expanded.  Relative paths are rejected â€” cron jobs run detached
        from any shell cwd, so relative paths have no stable meaning.
      - The path must exist and be a directory at create/update time.  We do
        NOT re-check at run time (a user might briefly unmount the dir; the
        scheduler will just fall back to old behaviour with a logged warning).

    Returns the absolute path string, or None when disabled.
    Raises ValueError on invalid input.
    Nz+Cron workdir must be an absolute path (got zN). Cron jobs run detached from any shell cwd, so relative paths are ambiguous.zCron workdir does not exist: z!Cron workdir is not a directory: )	r�   rM   r   r@   rŽ   rP   r9   rÌ   Úis_dir)r¬  rU   ÚexpandedÚresolveds       r   Ú_normalize_workdirr±  v  sü   € ð €ØˆtÝ
ˆg‰,Œ,×
Ò
Ñ
Ô
€CØð ØˆtÝ�C‰yŒy×#Ò#Ñ%Ô%€HØ×ÒÑ!Ô!ð 
Ýð[¸#ð [ð [ð [ñ
ô 
ð 	
ð ×ÒÑ!Ô!€HØ�?Š?ÑÔð EÝÐC¸ÐCÐCÑDÔDÐDØ�?Š?ÑÔð IÝÐG¸XÐGÐGÑHÔHÐHÝˆx‰=Œ=Ðr/   c                  ób  — 	 ddl m} m} t          ¦   «         dz  }|                     ¦   «         sdS  ||¦  «        }	 ddlm} |                     |¦  «        }n# t          $ r Y nw xY w | |¦  «        }| 	                    d¦  «        pi }t          |t          ¦  «        rR| 	                    d¦  «        }t          |t          ¦  «        r(|                     ¦   «         r|                     ¦   «         S | 	                    d¦  «        pi }t          |t          ¦  «        r|                     ¦   «         pdS t          |t          ¦  «        rU| 	                    d¦  «        p| 	                    d¦  «        }t          |t          ¦  «        r|                     ¦   «         pdS dS # t          $ r Y dS w xY w)	uc  Resolve the global default model the same way the cron ticker does.

    Mirrors the unpinned-model resolution in ``cron/scheduler.py`` ``run_job``:
    read ``config.yaml`` ``model.default`` (or the ``model`` alias / bare string
    form), applying the managed-scope overlay and env expansion. Used by
    ``create_job`` to snapshot the default model for unpinned jobs so a later
    swap of the global default is detected at fire time (#44585).

    Returns the resolved model string, or ``None`` if config is missing/empty
    or resolution fails (fail-open â€” caller treats ``None`` as "no snapshot").
    r   )Ú_expand_env_varsÚread_user_config_rawzconfig.yamlN)Úmanaged_scoper   Úmodelr1   )Úhermes_cli.configr³  r´  r   rÌ   Ú
hermes_clirµ  Úapply_managed_overlayr^   r4   r•   rž   r�   rM   )	r³  r´  Úcfg_pathÚcfgrµ  Úcron_cfgÚ
cron_modelÚ	model_cfgr1   s	            r   Ú_resolve_default_model_snapshotr¿  —  sß  € ðØLÐLÐLÐLÐLÐLÐLÐLå"Ñ$Ô$ }Ñ4ˆØ�ŠÑ Ô ð 	Ø�4Ø"Ð" 8Ñ,Ô,ˆð	Ø0Ð0Ð0Ð0Ð0Ð0Ø×5Ò5°cÑ:Ô:ˆCˆCøÝð 	ð 	ð 	ØˆDð	øøøàÐ˜sÑ#Ô#ˆð —7’7˜6‘?”?Ð( bˆÝ�h¥Ñ%Ô%ð 	*Ø!Ÿš gÑ.Ô.ˆJÝ˜*¥cÑ*Ô*ð *¨z×/?Ò/?Ñ/AÔ/Að *Ø!×'Ò'Ñ)Ô)Ð)Ø—G’G˜GÑ$Ô$Ð*¨ˆ	Ý�i¥Ñ%Ô%ð 	-Ø—?’?Ñ$Ô$Ð,¨Ð,Ý�i¥Ñ&Ô&ð 	/Ø—m’m IÑ.Ô.ÐH°)·-²-ÀÑ2HÔ2HˆGÝ˜'¥3Ñ'Ô'ð /Ø—}’}‘”Ð.¨$Ð.ØˆtøÝð ð ð ØˆtˆtðøøøsH   ‚-F  ±F  ½A ÁF  Á
A&Á#F  Á%A&Á&BF  Ã2AF  Ä4A)F  Æ 
F.Æ-F.©Ústrip_trailing_slashrÁ  c                óŽ   — t          | t          ¦  «        sd S |                      ¦   «         }|r|                     d¦  «        }|pd S )Nr‹   )r•   r�   rM   Úrstrip)r    rÁ  r�   s      r   Ú_normalize_job_optional_textrÄ  Ã  sI   € Ý�e�SÑ!Ô!ð ØˆtØ�;Š;‰=Œ=€DØð  Ø�{Š{˜3ÑÔˆØˆ<�4Ðr/   Úproviderr¶  Úbase_urlÚno_agentc                 óÔ  — t          | ¦  «        }t          |¦  «        }t          |d¬¦  «        }t          |¦  «        rdS d}d}|€y	 ddlm}	 ddi}
|r||
d<    |	di |
¤Ž}t	          |                     d	¦  «        pd
¦  «                             ¦   «                              ¦   «         }|pd}n# t          $ r d}Y nw xY w|€$	 t          ¦   «         pd}n# t          $ r d}Y nw xY w||fS )ap  Snapshot unpinned inference axes for the provider/model drift guard.

    Agent cron jobs with unpinned provider/model follow global config at fire
    time. Capture the current resolution for each unpinned axis so a later
    global switch fails closed instead of silently changing spend. Pinned axes
    and no-agent script jobs intentionally carry no snapshot.
    TrÀ  ©NNNr   )Úresolve_runtime_providerÚ	requestedÚexplicit_base_urlrÅ  rJ   r.   )
rÄ  r   Úhermes_cli.runtime_providerrÊ  r�   r4   rM   rå   r^   r¿  )rÅ  r¶  rÆ  rÇ  Únormalized_providerÚnormalized_modelÚnormalized_base_urlÚprovider_snapshotÚmodel_snapshotrÊ  Úruntime_kwargsÚsnapÚsnap_providers                r   Ú!_compute_provider_model_snapshotsrÖ  Ì  sc  € õ 7°xÑ@Ô@ÐÝ3°EÑ:Ô:ÐÝ6ØØ!ðñ ô Ðõ ˆH�~„~ð Øˆzà'+ÐØ$(€NØÐ"ð
	%ØLÐLÐLÐLÐLÐLà)¨4Ð0ˆNØ"ð JØ6I�Ð2Ñ3Ø+Ð+Ð=Ð=¨nÐ=Ð=ˆDÝ §¢¨Ñ 4Ô 4Ð :¸Ñ;Ô;×AÒAÑCÔC×IÒIÑKÔKˆMØ -Ð 5°ÐÐøÝð 	%ð 	%ð 	%Ø $ÐÐÐð	%øøøàÐð	"Ý<Ñ>Ô>ÐFÀ$ˆNˆNøÝð 	"ð 	"ð 	"Ø!ˆNˆNˆNð	"øøøà˜nÐ,Ð,s%   ÁA%B. Â.B=Â<B=ÃC ÃC#Ã"C#c                 ó  — t          |                      d¦  «        ¦  «        t          |                      d¦  «        ¦  «        t          |                      d¦  «        d¬¦  «        t          |                      d¦  «        ¦  «        fS )zBReturn the stored inference-routing fields in their semantic form.rÅ  r¶  rÆ  TrÀ  rÇ  )rÄ  r4   r   r¼   s    r   Ú_normalized_inference_axesrØ  ù  so   € õ 	% S§W¢W¨ZÑ%8Ô%8Ñ9Ô9Ý$ S§W¢W¨WÑ%5Ô%5Ñ6Ô6Ý$ S§W¢W¨ZÑ%8Ô%8ÈtÐTÑTÔTÝˆS�WŠW�ZÑ Ô Ñ!Ô!ð	ð r/   Úmonitor_scriptÚmonitor_urlr²   c                 ó€   — | r|rt          d¦  «        ‚| s|r|rt          d¦  «        ‚|r|st          d¦  «        ‚dS dS )a.  Shared create/update validation for job execution-mode invariants.

    ONE owner for the class: create_job and update_job both call this so an
    invariant enforced at create time cannot be violated through the update
    door (monitor jobs silently degrading when no_agent is flipped on, etc.).
    ua   monitor_script and monitor_url are mutually exclusive â€” a job can only have one monitor source.uÈ   monitor_script/monitor_url cannot be combined with no_agent=True â€” the whole point of a monitor job is to suppress or wake the AGENT based on source changes. Use a plain no_agent script job instead.ud   no_agent=True requires a script â€” with no agent and no script there is nothing for the job to run.N)rP   )rÙ  rÚ  rÇ  r²   s       r   Ú_validate_job_mode_invariantsrÜ    s–   € ð ð 
˜+ð 
Ýð0ñ
ô 
ð 	
ð 	ð 
˜+ð 
¨8ð 
ÝðPñ
ô 
ð 	
ð
 ð 
˜ð 
Ýð3ñ
ô 
ð 	
ð
ð 
ð 
ð 
r/   r°   r±   ÚrepeatÚdeliverÚoriginÚcontext_fromÚenabled_toolsetsÚattach_to_sessionc                 ó*  — t          |¦  «        }|�|dk    rd}|d         dk    r|€d}|€|rdnd}t          j        ¦   «         j        dd…         }t	          ¦   «                              ¦   «         }t          ||¦  «        }t          |¦  «        }t          |	¦  «        }t          |
d	¬
¦  «        }t          |t          ¦  «        r!t          |¦  «         
                    ¦   «         nd}|pd}|rd„ |D ¦   «         nd}|pd}t          |¦  «        }t          |¦  «        }t          |t          ¦  «        r|nd}t          |t          ¦  «        r!t          |¦  «         
                    ¦   «         nd}|pd}t          |t          ¦  «        r!t          |¦  «         
                    ¦   «         nd} | pd} t          || ||¦  «         t          |t          ¦  «        r,| 
                    ¦   «         r| 
                    ¦   «         gnd}n&t          |t          ¦  «        rd„ |D ¦   «         pd}nd}t          | ¦  «        }!ddlm}"  |"|!|¦  «         |!p|r|d         ndp|r|ndpd}#t%          ||||¬¦  «        \  }$}%t'          |¦  «        }&|                     d¦  «        dk    rr|&€p|                     d¦  «        p|}'t*                               d|p|#dd…          
                    ¦   «         |'t.          ¦  «         t1          d|'› dt.          › d�¦  «        ‚i d|“d|p|#dd…          
                    ¦   «         “d|!“d|“d|r|d         nd“d|“d|“d|$“d|%“d|“d |“d!|“d"|“d#| “d$d“d%|“d&|“i d'|                     d(|¦  «        “d)|dd*œ“d+d	“d,d-“d.d“d/d“d0|“d1|&“d2d“d3d“d4d“d5d“d6|“d|“d7|“d8|“¥}(|�||(d9<   t3          ¦   «         5  t5          ¦   «         })|)                     |(¦  «         t9          |)¦  «         ddd¦  «         n# 1 swxY w Y   |(S ):u!  
    Create a new cron job.

    Args:
        prompt: The prompt to run (must be self-contained, or a task instruction when skill is set).
                Ignored when ``no_agent=True`` except as an optional name hint.
        schedule: Schedule string (see parse_schedule)
        name: Optional friendly name
        repeat: How many times to run (None = forever, 1 = once)
        deliver: Where to deliver output ("origin", "local", "telegram", etc.)
        origin: Source info where job was created (for "origin" delivery)
        skill: Optional legacy single skill name to load before running the prompt
        skills: Optional ordered list of skills to load before running the prompt
        model: Optional per-job model override
        provider: Optional per-job provider override
        base_url: Optional per-job base URL override
        script: Optional path to a script whose stdout feeds the job. With
                ``no_agent=True`` the script IS the job â€” its stdout is
                delivered verbatim. Without ``no_agent``, its stdout is
                injected into the agent's prompt as context (data-collection /
                change-detection pattern). Paths resolve under
                ~/.hermes/scripts/; ``.sh`` / ``.bash`` files run via bash,
                anything else via Python.
        context_from: Optional job ID (or list of job IDs) whose most recent output
                      is injected into the prompt as context before each run.
                      Useful for chaining cron jobs: job A finds data, job B processes it.
        enabled_toolsets: Optional list of toolset names to restrict the agent to.
                          When set, only tools from these toolsets are loaded, reducing
                          token overhead. When omitted, all default tools are loaded.
                          Ignored when ``no_agent=True``.
        workdir: Optional absolute path.  When set, the job runs as if launched
                from that directory: AGENTS.md / CLAUDE.md / .cursorrules from
                that directory are injected into the system prompt, and the
                terminal/file/code_exec tools use it as their working directory
                (via TERMINAL_CWD).  When unset, the old behaviour is preserved
                (no context files injected, tools use the scheduler's cwd).
                With ``no_agent=True``, ``workdir`` is still applied as the
                script's cwd so relative paths inside the script behave
                predictably.
        no_agent: When True, skip the agent entirely â€” run ``script`` on schedule
                and deliver its stdout directly. Empty stdout = silent (no
                delivery). Requires ``script`` to be set. Ideal for classic
                watchdogs and periodic alerts that don't need LLM reasoning.
        monitor_script: Optional path to a cheap monitor source script (same
                resolution/containment rules as ``script``: relative to
                ~/.hermes/scripts/, .sh/.bash via bash, else Python). Each
                tick the script runs FIRST and its output is hashed as exact
                bytes: unchanged output suppresses the agent run entirely
                (recorded as a silent 'no_change' tick); changed output
                injects a MONITOR CHANGE DETECTED block (unified diff + new
                output) into the prompt before a normal agent run. Scripts
                should emit stable output (no timestamps). Mutually exclusive
                with ``monitor_url``; incompatible with ``no_agent=True``.
        monitor_url: Optional http(s) URL used as the monitor source instead
                of a script â€” fetched with a bounded GET each tick. Same
                hash-suppression semantics as ``monitor_script``.

    Returns:
        The created job dict
    Nr   rð   rû   rf   rß  Úlocalé   TrÀ  c                 ó’   — g | ]D}t          |¦  «                             ¦   «         ¯#t          |¦  «                             ¦   «         ‘ŒES r.   ©r�   rM   )rô   Úts     r   r�  zcreate_job.<locals>.<listcomp>‰  s9   € ÐVÐVÐV¨aÅsÈ1ÁvÄvÇ|Â|Á~Ä~ÐV�3˜q™6œ6Ÿ<š<™>œ>ÐVÐVÐVr/   c                 ó’   — g | ]D}t          |¦  «                             ¦   «         ¯#t          |¦  «                             ¦   «         ‘ŒES r.   rç  r‹  s     r   r�  zcreate_job.<locals>.<listcomp>¤  s9   € ÐOÐOÐO¨1ÅÀAÁÄÇÂÁÄÐO�˜A™œŸš™œÐOÐOÐOr/   )Úcheck_gateway_lifecycler³   ©rÅ  r¶  rÆ  rÇ  rª   zKRejecting one-shot cron job '%s': run_at %s is outside the %ss grace windowr´   úRequested one-shot time ú is more than ú&s in the past and cannot be scheduled.r‡   r±   r°   r“   r’   r¶  rÅ  rÑ  rÒ  rÆ  r²   rÇ  rÙ  rÚ  Úmonitor_staterà  r§   r¦   r¨   rÝ  )ÚtimesrÂ   r¿   rµ   rÃ   r»   Úpaused_reasonÚ
created_atÚnext_run_atr  Úlast_statusÚ
last_errorÚlast_delivery_errorrÞ  rá  r¬  râ  )r  ÚuuidÚuuid4Úhexr  r  r›   rÄ  r•   r�   rM   r±  r   rÜ  r–   r¤   Úcron.lifecycle_guardrê  rÖ  r/  r4   r_   r`   r  rP   r†   ru  r—   rp  )*r°   r§   r±   rÝ  rÞ  rß  r’   r“   r¶  rÅ  rÆ  r²   rà  rá  r¬  rÇ  râ  rÙ  rÚ  Úparsed_schedulerX   r   Únormalized_skillsrÏ  rÎ  rÐ  Únormalized_scriptÚnormalized_toolsetsÚnormalized_workdirÚnormalized_no_agentÚnormalized_attachÚnormalized_monitor_scriptÚnormalized_monitor_urlÚprompt_textrê  r·   rÑ  rÒ  ró  rª   rœ   rl  s*                                             r   Ú
create_jobr  !  sl  € õb % XÑ.Ô.€Oð Ð˜f¨šk˜kØˆð �vÔ &Ò(Ð(¨V¨^Øˆð €Ø$Ð1�(�(¨'ˆåŒZ‰\Œ\Ô˜c˜r˜cÔ"€FÝ
‰-Œ-×
!Ò
!Ñ
#Ô
#€Cå-¨e°VÑ<Ô<ÐÝ3°EÑ:Ô:ÐÝ6°xÑ@Ô@ÐÝ6°xÐVZÐ[Ñ[Ô[ÐÝ/9¸&Å#Ñ/FÔ/FÐP�˜F™œ×)Ò)Ñ+Ô+Ð+ÈDÐØ)Ð1¨TÐØZjÐtÐVÐVÐ3CÐVÑVÔVÐVÐptÐØ-Ð5°ÐÝ+¨GÑ4Ô4ÐÝ˜x™.œ.ÐÝ-7Ð8IÍ4Ñ-PÔ-PÐZÐ)Ð)ÐVZÐÝ?IÈ.ÕZ]Ñ?^Ô?^Ð h¥ NÑ 3Ô 3× 9Ò 9Ñ ;Ô ;Ð ;ÐdhÐØ 9Ð A¸TÐÝ9CÀKÕQTÑ9UÔ9UÐ_�S Ñ-Ô-×3Ò3Ñ5Ô5Ð5Ð[_ÐØ3Ð;°tÐõ "Ø!ØØØñ	ô ð õ �,¥Ñ$Ô$ð Ø1=×1CÒ1CÑ1EÔ1EÐO˜×*Ò*Ñ,Ô,Ð-Ð-È4ˆˆÝ	�L¥$Ñ	'Ô	'ð ØOÐO°ÐOÑOÔOÐWÐSWˆˆàˆå" 6Ñ*Ô*€Kð =Ð<Ð<Ð<Ð<Ð<ØÐ˜KÐ):Ñ;Ô;Ð;àð  QÐ<MÐ$WÐ$5°aÔ$8Ð$8ÐSWð  Qð  sFð  ^PÐ]nÐ]nð  LPð  `ð  V`€Lå(IØ$ØØ$Ø$ð	)ñ )ô )Ñ%Ð�~õ # ?Ñ3Ô3€KØ×Ò˜6Ñ"Ô" fÒ,Ð,°Ð1DØ ×$Ò$ XÑ.Ô.Ð:°(ˆÝ�ŠØYØÐ-�L  " Ô%×+Ò+Ñ-Ô-ØÝ!ñ		
ô 	
ð 	
õ ðM vð Mð MÝ$ðMð Mð Mñ
ô 
ð 	
ð
+Øˆfð+à�Ð1˜ S b SÔ)×/Ò/Ñ1Ô1ð+ð 	�+ð+ð 	Ð#ð	+ð
 	Ð):ÐDÐ" 1Ô%Ð%Àð+ð 	Ð!ð+ð 	Ð'ð+ð 	Ð.ð+ð 	˜.ð+ð 	Ð'ð+ð 	Ð#ð+ð 	Ð'ð+ð  	Ð3ð!+ð" 	Ð-ð#+ð( 	˜ð)+ð* 	˜ð++ð, 	�Oð-+ð +ð. 	˜O×/Ò/°	¸8ÑDÔDð/+ð0 	ØØð
ð 
ð1+ð8 	�4ð9+ð: 	�ð;+ð< 	�Tð=+ð> 	˜ð?+ð@ 	�cðA+ðB 	�{ðC+ðD 	�tðE+ðF 	�tðG+ðH 	�dðI+ðJ 	˜tðK+ðN 	�7ðO+ðP 	�&ðQ+ðR 	Ð/ðS+ðT 	Ð%ðU+ð +€Cð^ Ð$Ø#4ˆÐÑ å	‰Œð ð Ý‰{Œ{ˆØ�Š�CÑÔÐÝ�$‰Œˆðð ð ñ ô ð ð ð ð ð ð øøøð ð ð ð ð
 €Js   Ï	3PÐPÐPc                 óf   — t          ¦   «         }|D ]}|d         | k    rt          |¦  «        c S Œ dS )zGet a job by ID.r‡   N)ru  r¸   )rX   rl  rœ   s      r   Úget_jobr    sG   € å‰;Œ;€DØð .ð .ˆØˆtŒ9˜ÒÐÝ(¨Ñ-Ô-Ð-Ð-Ð-ð àˆ4r/   c                   óH   ‡ — e Zd ZdZdedeeeef                  fˆ fd„Zˆ xZ	S )ÚAmbiguousJobReferencez1Raised when a job name matches more than one job.ÚrefÚmatchesc           	      óÒ   •— || _         || _        d                     d„ |D ¦   «         ¦  «        }t          ¦   «                              d|› dt          |¦  «        › d|› d�¦  «         d S )Nú, c              3   ó&   K  — | ]}|d          V — ŒdS rŸ  r.   )rô   râ   s     r   rö   z1AmbiguousJobReference.__init__.<locals>.<genexpr>  s&   è è € Ð1Ð1 A˜˜$œÐ1Ð1Ð1Ð1Ð1Ð1r/   z
Job name 'u   ' is ambiguous â€” matches z jobs: z. Use the job ID instead.)r
  r  r`  ÚsuperÚ__init__rÿ   )Úselfr
  r  ÚidsÚ	__class__s       €r   r  zAmbiguousJobReference.__init__  s‹   ø€ ØˆŒØˆŒØ�iŠiÐ1Ð1¨Ð1Ñ1Ô1Ñ1Ô1ˆÝ‰Œ×Òð'˜ð 'ð '½¸W¹¼ð 'ð 'Ècð 'ð 'ð 'ñ	
ô 	
ð 	
ð 	
ð 	
r/   )
r*   r+   r,   Ú__doc__r�   r   r
   r   r  Ú__classcell__)r  s   @r   r	  r	    s^   ø€ € € € € Ø;Ð;ð
˜Cð 
¨$¨t°C¸°H¬~Ô*>ð 
ð 
ð 
ð 
ð 
ð 
ð 
ð 
ð 
ð 
r/   r	  r
  c                 ó<  ‡— | sdS t          ¦   «         }|D ]}|d         | k    rt          |¦  «        c S Œ |                      ¦   «         Šˆfd„|D ¦   «         }|sdS t          |¦  «        dk    rt	          | d„ |D ¦   «         ¦  «        ‚t          |d         ¦  «        S )aU  Resolve a job reference (ID or name) to a job record.

    - Exact ID match wins (works even if a different job's name equals this ID).
    - Otherwise, case-insensitive name match.
    - If a name matches more than one job, raises AmbiguousJobReference so the
      caller can surface the matching IDs rather than silently picking one.
    Nr‡   c                 ón   •— g | ]1}|                      d ¦  «        pd                     ¦   «         ‰k    ¯/|‘Œ2S )r±   rJ   )r4   rå   )rô   rŒ  Ú	ref_lowers     €r   r�  z#resolve_job_ref.<locals>.<listcomp>(  s>   ø€ ÐRÐRÐR˜!¨¯ª¨f©¬Ð(;¸×'BÒ'BÑ'DÔ'DÈ	Ò'QÐ'Q�AÐ'QÐ'QÐ'Qr/   rf   c                 ó,   — g | ]}t          |¦  «        ‘ŒS r.   ©r¸   r‹  s     r   r�  z#resolve_job_ref.<locals>.<listcomp>-  s!   € ÐAÐAÐA¨qÕ'¨Ñ*Ô*ÐAÐAÐAr/   r   )ru  r¸   rå   rÿ   r	  )r
  rl  rœ   Úname_matchesr  s       @r   Úresolve_job_refr    sÒ   ø€ ð ð ØˆtÝ‰;Œ;€DØð .ð .ˆØˆtŒ9˜ÒÐÝ(¨Ñ-Ô-Ð-Ð-Ð-ð à—	’	‘”€IØRÐRÐRÐR˜tÐRÑRÔR€LØð ØˆtÝ
ˆ<ÑÔ˜1ÒÐÝ#ØÐAÐA°LÐAÑAÔAñ
ô 
ð 	
õ ! ¨a¤Ñ1Ô1Ð1r/   Úinclude_disabledc                 ó  — d„ t          ¦   «         D ¦   «         }| sd„ |D ¦   «         }	 ddlm}  |d„ |D ¦   «         ¦  «        }n# t          $ r i }Y nw xY w|D ].}|                     |                     dd¦  «        ¦  «        |d<   Œ/|S )	z2List all jobs, optionally including disabled ones.c                 ó,   — g | ]}t          |¦  «        ‘ŒS r.   r  r‹  s     r   r�  zlist_jobs.<locals>.<listcomp>4  s!   € Ð:Ð:Ð:¨Õ! !Ñ$Ô$Ð:Ð:Ð:r/   c                 ó>   — g | ]}|                      d d¦  «        ¯|‘ŒS )r¿   TrŠ  r‹  s     r   r�  zlist_jobs.<locals>.<listcomp>6  s+   € Ð:Ð:Ð:�a 1§5¢5¨°DÑ#9Ô#9Ð:�Ð:Ð:Ð:r/   r   )Úlatest_executionsc                 ó:   — g | ]}|                      d d¦  «        ‘ŒS )r‡   rJ   rŠ  )rô   rœ   s     r   r�  zlist_jobs.<locals>.<listcomp>:  s&   € Ð#FÐ#FÐ#F¸# C§G¢G¨D°"Ñ$5Ô$5Ð#FÐ#FÐ#Fr/   r‡   rJ   Úlatest_execution)ru  Úcron.executionsr!  r^   r4   )r  rl  r!  Úlatestrœ   s        r   Ú	list_jobsr&  2  sÎ   € à:Ð:­i©k¬kÐ:Ñ:Ô:€DØð ;Ø:Ð:˜4Ð:Ñ:Ô:ˆðØ5Ð5Ð5Ð5Ð5Ð5à"Ð"Ð#FÐ#FÀÐ#FÑ#FÔ#FÑGÔGˆˆøÝð ð ð Øˆˆˆðøøøàð @ð @ˆØ"(§*¢*¨S¯WªW°T¸2Ñ->Ô->Ñ"?Ô"?ˆÐÑÐØ€Ks   ¨A ÁAÁAÚupdatesc                 ó$
  — t                                |pi ¦  «        }|r2t          dd                     t	          |¦  «        ¦  «        › �¦  «        ‚t          ¦   «         5  t          ¦   «         }t          |¦  «        D �]y\  }}|d         | k    rŒd|v r$|d         }|dv rd|d<   nt          |¦  «        |d<   dD ]M}||v rG||         }t          |t          ¦  «        r!t          |¦  «                             ¦   «         nd}|pd||<   ŒNt          |¦  «        }	t          i |¥|¥¦  «        }
h d£                     |¦  «        r«|
                     d	¦  «        }t          |t          ¦  «        r!t          |¦  «                             ¦   «         nd}t          |
                     d
¦  «        pd|
                     d¦  «        pdt!          |
                     d¦  «        ¦  «        |pd¦  «         d|v }t!          h d£                     |¦  «        ¦  «        ot          |
¦  «        |	k    }d|v sd|v rJt#          |
                     d¦  «        |
                     d¦  «        ¦  «        }||
d<   |r|d         nd|
d<   |�r!|
d         }t          |t          ¦  «        rt%          |¦  «        }||
d<   |                     d|                     d|
                     d¦  «        ¦  «        ¦  «        |
d<   |
                     d¦  «        dk    r—t'          |¦  «        }|€�|                     d¦  «        dk    rh|                     d¦  «        p|}t(                               d|
                     d| ¦  «        |t,          ¦  «         t          d|› dt,          › d�¦  «        ‚||
d<   |rlt/          |
                     d¦  «        |
                     d ¦  «        |
                     d!¦  «        |
                     d¦  «        ¬"¦  «        \  }}||
d#<   ||
d$<   |
                     d%d&¦  «        r |
                     d¦  «        dk    r‡|
                     d¦  «        srt'          |
d         ¦  «        }|€V|
d                              d¦  «        dk    r7|
d                              dd'¦  «        }t          d|› d(t,          › d)�¦  «        ‚||
d<   |
||<   t1          |¦  «         t3          ||         ¦  «        c cddd¦  «         S 	 ddd¦  «         n# 1 swxY w Y   dS )*zCUpdate a job by ID, refreshing derived schedule fields when needed.z%Cron job field(s) cannot be updated: r  r‡   r¬  >   FNrJ   N)rÙ  rÚ  >   r²   rÇ  rÚ  rÙ  r²   rÙ  rÚ  rÇ  r§   >   r¶  rÆ  rÇ  rÅ  r“   r’   r   r¦   r¨   rµ   rº   rð   rû   rª   zRRejecting one-shot cron job update '%s': run_at %s is outside the %ss grace windowr±   rì  rí  rî  ró  rÅ  r¶  rÆ  rë  rÑ  rÒ  r¿   Tr¯   ú is in the past (grace window: zs) and cannot be scheduled.)Ú_IMMUTABLE_JOB_FIELDSÚintersectionrP   r`  Úsortedr†   ru  Ú	enumerater±  r•   r�   rM   rØ  rŸ   r4   rÜ  r   r›   r  r/  r_   r`   r  rÖ  rp  r¸   )rX   r'  Ú
bad_fieldsrl  r†  rœ   Ú_wdÚ
_mon_fieldÚ_mvÚprevious_inference_axesÚupdatedÚ_upd_scriptÚschedule_changedÚinference_fields_changedrü  Úupdated_scheduleÚupdated_next_runrª   rÑ  rÒ  r-  s                        r   Ú
update_jobr9  B  sB  € õ
 '×3Ò3°G°M¸rÑBÔB€JØð 
ÝØS°D·I²I½fÀZÑ>PÔ>PÑ4QÔ4QÐSÐSñ
ô 
ð 	
õ 
‰Œð o2ð o2Ý‰{Œ{ˆÝ ‘o”oð m	2ñ m	2‰FˆAˆsØ�4Œy˜FÒ"Ð"Øð ˜GÐ#Ð#Ø˜iÔ(�ØÐ+Ð+Ð+Ø)-�G˜IÑ&Ð&å);¸CÑ)@Ô)@�G˜IÑ&ð @ð 6ð 6�
Ø Ð(Ð(Ø! *Ô-�CÝ.8¸½cÑ.BÔ.BÐL�#˜c™(œ(Ÿ.š.Ñ*Ô*Ð*È�CØ*-¨+°�G˜JÑ'øå&@ÀÑ&EÔ&EÐ#Ý)Ð*<¨SÐ*<°GÐ*<Ñ=Ô=ˆGð GÐFÐF×SÒSÐT[Ñ\Ô\ð Ø%Ÿkšk¨(Ñ3Ô3�Ý:DÀ[ÕRUÑ:VÔ:VÐ`�c +Ñ.Ô.×4Ò4Ñ6Ô6Ð6Ð\`�Ý-Ø—K’KÐ 0Ñ1Ô1Ð9°TØ—K’K Ñ.Ô.Ð6°$Ý˜Ÿš ZÑ0Ô0Ñ1Ô1ØÐ' 4ñ	ô ð ð  *¨WÐ4ÐÝ'+Ø=Ð=Ð=×JÒJÈ7ÑSÔSñ(ô (ð (Qå,¨WÑ5Ô5Ð9PÒPð %ð ˜7Ð"Ð" g°Ð&8Ð&8Ý$9¸'¿+º+ÀgÑ:NÔ:NÐPW×P[ÒP[Ð\dÑPeÔPeÑ$fÔ$fÐ!Ø$5�˜Ñ!Ø;LÐ#VÐ#4°QÔ#7Ð#7ÐRV�˜Ñ àñ #>Ø#*¨:Ô#6Ð õ Ð.µÑ4Ô4ð ;Ý'5Ð6FÑ'GÔ'GÐ$Ø*:�G˜JÑ'Ø.5¯kªkØ&Ø$×(Ò(¨°G·K²KÐ@RÑ4SÔ4SÑTÔTñ/ô /�Ð*Ñ+ð —;’;˜wÑ'Ô'¨8Ò3Ð3Ý'7Ð8HÑ'IÔ'IÐ$ð )Ð0Ø,×0Ò0°Ñ8Ô8¸FÒBÐBà!1×!5Ò!5°hÑ!?Ô!?Ð!SÐCS˜ÝŸšð>à#ŸKšK¨°Ñ7Ô7Ø"Ý1ñô ð õ )ð]°vð ]ð ]Ý4ð]ð ]ð ]ñô ð ð .>�G˜MÑ*à'ð ;Ý4UØ$Ÿ[š[¨Ñ4Ô4Ø!Ÿ+š+ gÑ.Ô.Ø$Ÿ[š[¨Ñ4Ô4Ø$Ÿ[š[¨Ñ4Ô4ð	5ñ 5ô 5Ñ1Ð! >ð 0A�Ð+Ñ,Ø,:�Ð(Ñ)à�{Š{˜9 dÑ+Ô+ð 2°·²¸GÑ0DÔ0DÈÒ0PÐ0PÐY`×YdÒYdÐerÑYsÔYsÐ0PÝ+¨G°JÔ,?Ñ@Ô@�ØÐ#¨°
Ô(;×(?Ò(?ÀÑ(GÔ(GÈ6Ò(QÐ(QØ$ ZÔ0×4Ò4°X¸yÑIÔI�FÝ$ð]°6ð ]ð ]Ý*?ð]ð ]ð ]ñô ð ð *2�˜Ñ&àˆD�‰GÝ�d‰OŒOˆOÝ(¨¨a¬Ñ1Ô1Ð1Ð1ð_o2ð o2ð o2ð o2ñ o2ô o2ð o2ð o2ðm	2ðo2ð o2ð o2ñ o2ô o2ð o2ð o2ð o2ð o2ð o2ð o2øøøð o2ð o2ð o2ð o2ð` ˆ4s   ÁRTÔT	ÔT	Úreasonc                 óš   — t          | ¦  «        }|sdS t          |d         ddt          ¦   «                              ¦   «         |dœ¦  «        S )z:Pause a job without deleting it. Accepts a job ID or name.Nr‡   Frº   )r¿   rµ   r»   rñ  ©r  r9  r  r  )rX   r:  rœ   s      r   Ú	pause_jobr=  À  s[   € å
˜&Ñ
!Ô
!€CØð ØˆtÝØˆDŒ	àØÝ$™œ×0Ò0Ñ2Ô2Ø#ð		
ð 	
ñô ð r/   c           	      ó:  — t          | ¦  «        }|sdS t          |d         ¦  «        }|€V|d                              d¦  «        dk    r7|d                              dd¦  «        }t          d|› dt          › d	�¦  «        ‚t          |d
         dddd|dœ¦  «        S )zWResume a paused job and compute the next future run from now. Accepts a job ID or name.Nr§   rð   rû   rª   r¯   zCannot resume: one-shot time r)  zs) and will never fire.r‡   TrÃ   ©r¿   rµ   r»   rñ  ró  )r  r/  r4   rP   r  r9  )rX   rœ   ró  rª   s       r   Ú
resume_jobr@  Ð  s×   € å
˜&Ñ
!Ô
!€CØð Øˆtå" 3 z¤?Ñ3Ô3€KØÐ˜s :œ×2Ò2°6Ñ:Ô:¸fÒDÐDØ�Z”×$Ò$ X¨yÑ9Ô9ˆÝðM¨Fð Mð MÝ3ðMð Mð Mñ
ô 
ð 	
õ ØˆDŒ	àØ ØØ!Ø&ð	
ð 	
ñ	ô 	ð 	r/   c           	      óœ   — t          | ¦  «        }|sdS t          |d         ddddt          ¦   «                              ¦   «         dœ¦  «        S )zKSchedule a job to run on the next scheduler tick. Accepts a job ID or name.Nr‡   TrÃ   r?  r<  )rX   rœ   s     r   Útrigger_jobrB  é  s^   € å
˜&Ñ
!Ô
!€CØð ØˆtÝØˆDŒ	àØ ØØ!Ý&™=œ=×2Ò2Ñ4Ô4ð	
ð 	
ñ	ô 	ð 	r/   c                 ó6  ‡— t          | ¦  «        }|sdS |d         Št          ¦   «         5  t          ¦   «         }t          |¦  «        }ˆfd„|D ¦   «         }t          |¦  «        |k     r—t	          ‰¦  «        }t          |‰h¬¦  «         |                     ¦   «         rt          j        |¦  «         	 ddl	m
}  |‰¦  «         n-# t          $ r  t                               d‰d¬	¦  «         Y nw xY w	 d
d
d
¦  «         dS 	 d
d
d
¦  «         n# 1 swxY w Y   dS )zRemove a job by ID or name.Fr‡   c                 ó,   •— g | ]}|d          ‰k    ¯|‘ŒS r‰  r.   )rô   rŒ  Úcanonical_ids     €r   r�  zremove_job.<locals>.<listcomp>  s'   ø€ Ð;Ð;Ð;�a 1 T¤7¨lÒ#:Ð#:�Ð#:Ð#:Ð#:r/   r�  r   )Úclear_notepadz*Failed to clear notepad for removed job %sTr[   N)r  r†   ru  rÿ   r‘   rp  rÌ   ÚshutilÚrmtreeÚcron.notepadrF  r^   r_   Údebug)rX   rœ   rl  Úoriginal_lenÚjob_output_dirrF  rE  s         @r   Ú
remove_jobrM  ú  s¶  ø€ å
˜&Ñ
!Ô
!€CØð ØˆuØ�t”9€LÝ	‰Œð ð Ý‰{Œ{ˆÝ˜4‘y”yˆØ;Ð;Ð;Ð;˜4Ð;Ñ;Ô;ˆÝˆt‰9Œ9�|Ò#Ð#õ -¨\Ñ:Ô:ˆNÝ�d¨¨Ð7Ñ7Ô7Ð7à×$Ò$Ñ&Ô&ð .Ý”˜nÑ-Ô-Ð-ðØ6Ð6Ð6Ð6Ð6Ð6Ø�˜lÑ+Ô+Ð+Ð+øÝð ð ð Ý—’Ø@Ø ¨4ð ñ ô ð ð ð ðøøøð
 ð1ð ð ñ ô ð ð ð ð $ð	ð ð ñ ô ð ð ð ð ð ð øøøð ð ð ð ð2 ˆ5s6   «BDÂ4CÃDÃ'C0Ã-DÃ/C0Ã0DÄDÄDc                 ó†  — t          ¦   «         5  t          ¦   «         }t          |¦  «        D ]{\  }}|d         | k    rjt          |                     d¦  «        ¦  «        }|rd|d<   n|                     dd¦  «         ||k    r|||<   t          |¦  «         |c cddd¦  «         S Œ|	 ddd¦  «         n# 1 swxY w Y   dS )a¡  Set/clear the preflight alert-dedup marker; return the PRIOR value.

    The marker records that the operator was already alerted about this
    job's blocked configuration, so the scheduler alerts exactly once and
    stays silent on subsequent ticks until the config heals (same
    alert-once shape as the dead-pin auto-pause in #73506). Persisted on
    the job record so the dedup survives gateway restarts.
    r‡   Úpreflight_alertedTNF)r†   ru  r-  r   r4   Úpoprp  )rX   r    rl  r†  rœ   Úpriors         r   Ú_set_preflight_alertedrR    s6  € õ 
‰Œð ð Ý‰{Œ{ˆÝ ‘o”oð 
	ð 
	‰FˆAˆsØ�4Œy˜FÒ"Ð"Ý˜SŸWšWÐ%8Ñ9Ô9Ñ:Ô:�Øð 7Ø/3�CÐ+Ñ,Ð,à—G’GÐ/°Ñ6Ô6Ð6Ø˜E’>�>Ø!�D˜‘GÝ˜d‘O”O�OØ��ðð ð ð ñ ô ð ð ð #ð
	ðð ð ñ ô ð ð ð ð ð ð øøøð ð ð ð ð ˆ5s   �BB6Â(B6Â6B:Â=B:c                 ó"   — t          | d¦  «        S )zAMark the job as preflight-alerted; return True if it already was.T©rR  ©rX   s    r   Úmark_preflight_alertedrV  5  s   € å! &¨$Ñ/Ô/Ð/r/   c                 ó&   — t          | d¦  «         dS )z@Clear the preflight alert-dedup marker (config validates again).FNrT  rU  s    r   Úclear_preflight_alertedrX  :  s   € å˜6 5Ñ)Ô)Ð)Ð)Ð)r/   r}   Údelivery_errorÚstatusc                 ób  — t          ¦   «         5  t          ¦   «         }t          |¦  «        D �]M\  }}|d         | k    �r:t          ¦   «                              ¦   «         }||d<   |p|rdnd|d<   |s|nd|d<   |r|                     dd¦  «         ||d	<   d|d
<   |                     d¦  «        �d|d<   |                     d¦  «        r¸|d         }	|	                     d¦  «        }
|	                     dd¦  «        }|                     di ¦  «                             d¦  «        }|dk    o|
duo|
dk    o|dk    }|s
|dz  }||	d<   |
�8|
dk    r2||
k    r,d|d<   d|d<   d|d<   t          |¦  «          ddd¦  «         dS t          |d         |¦  «        |d<   |d         €œ|                     di ¦  «                             d¦  «        }|dv rdd|d<   |                     d¦  «        sd|d<   t           
                    d|                     d|                     dd¦  «        ¦  «        |¦  «         n)d|d<   d|d<   n|                     d¦  «        dk    rd|d<   t          |¦  «          ddd¦  «         dS �ŒOt                               d| ¦  «         ddd¦  «         dS # 1 swxY w Y   dS ) u’  
    Mark a job as having been run.
    
    Updates last_run_at, last_status, increments completed count,
    computes next_run_at, and auto-deletes if repeat limit reached.

    ``delivery_error`` is tracked separately from the agent error â€” a job
    can succeed (agent produced output) but fail delivery (platform down).

    ``status`` overrides the derived ``last_status`` ("ok"/"error") with a
    specific terminal status for this run â€” e.g. ``"blocked_config"`` when
    the pre-dispatch configuration validation refused to run the agent
    (T1-26), so `cronjob list` distinguishes "your config is broken" from
    "the run itself failed".
    r‡   r  Úokr}   rô  Nrõ  rO  rö  Ú
fire_claimÚ	run_claimrÝ  rð  rÂ   r   r§   rð   rû   rf   Fr¿   rµ   ró  >   r   rï   ztFailed to compute next run for recurring schedule (is the 'croniter' package installed in the gateway's Python env?)zyJob '%s' (%s) could not compute next_run_at; leaving enabled and marking state=error so the job is not silently disabled.r±   r«   rº   rÃ   z0mark_job_run: job_id %s not found, skipping save)r†   ru  r-  r  r  rP  r4   rp  r/  r_   r}   r`   )rX   rG  r}   rY  rZ  rl  r†  rœ   r   rÝ  rð  rÂ   rð   Úpreclaimed_oneshots                 r   Úmark_job_runr`  ?  s  € õ$ 
‰Œð aSð aSÝ‰{Œ{ˆÝ ‘o”oð ]	ñ ]	‰FˆAˆsØ�4Œy˜FÒ"Ñ"Ý!‘m”m×-Ò-Ñ/Ô/�Ø%(��MÑ"Ø%+Ð%M¸Ð0L°°ÀW��MÑ"Ø18Ð$B E E¸d��LÑ!ð ð 7Ø—G’GÐ/°Ñ6Ô6Ð6à-;�Ð)Ñ*ð %)��LÑ!ð —7’7˜;Ñ'Ô'Ð3Ø'+�C˜Ñ$ð —7’7˜8Ñ$Ô$ð Ø  œ]�FØ"ŸJšJ wÑ/Ô/�EØ &§
¢
¨;¸Ñ :Ô :�IØŸ7š7 :¨rÑ2Ô2×6Ò6°vÑ>Ô>�Dà šð *Ø!¨Ð-ð*à! AšIð*ð &¨šMð	 'ð .ð 8Ø! Q™˜	Ø.7˜˜{Ñ+ð Ð(¨U°QªY¨Y¸9ÈÒ;MÐ;Mð */˜˜I™Ø'2˜˜G™Ø-1˜˜MÑ*Ý! $™œ˜ØðyaSð aSð aSñ aSô aSð aSð aSð aSõ~ &6°c¸*´oÀsÑ%KÔ%K��MÑ"ð �}Ô%Ð-ØŸ7š7 :¨rÑ2Ô2×6Ò6°vÑ>Ô>�DØÐ3Ð3Ð3Ø'.˜˜G™Ø"Ÿwšw |Ñ4Ô4ð ð!Jð   Ñ-õ
 Ÿšð<ð  ŸGšG F¨C¯GªG°D¸#Ñ,>Ô,>Ñ?Ô?Ø ñô ð ð ð */˜˜I™Ø'2˜˜G™˜Ø—W’W˜WÑ%Ô%¨Ò1Ð1Ø#.�C˜‘Lå˜$‘”�ØðaSð aSð aSñ aSô aSð aSð aSð aSñ #õ| 	�ŠÐIÈ6ÑRÔRÐRðCaSð aSð aSñ aSô aSð aSð aSð aSð aSð aSð aSð aSøøøð aSð aSð aSð aSð aSð aSs   �E&J$ÆC+J$É:J$Ê$J(Ê+J(c                 óF  — |                       d¦  «        �dS 	 |                       d¦  «        pi }|                       d¦  «        pi }d|                       d¦  «        › d|                       d¦  «        › d	|                      d
d¦  «        › d|                      dd¦  «        › d|                      dd¦  «        › d|                      dd¦  «        › dt          ¦   «                              ¦   «         › d�}t          |                       dd¦  «        |¦  «         t                               d|                       d|                       dd¦  «        ¦  «        ¦  «         dS # t          $ r:}t                               d|                       d¦  «        |¦  «         Y d}~dS d}~ww xY w)u¡  Leave an operator-visible trace when a wedged one-shot is removed.

    A finite one-shot whose dispatch was claimed (``repeat.completed`` >=
    ``repeat.times``) but which never reached ``mark_job_run`` (``last_run_at``
    is null) was interrupted mid-run â€” scheduler restart, gateway kill, or a
    non-Exception escape (#73973). The recovery guards remove such jobs so
    they stop appearing due, but a silent removal leaves the user with no
    output, no error, and no job record. Write a small diagnostic file into
    the job's output directory so the removal is observable and debuggable.

    Best-effort: diagnostics must never break the removal itself.
    r  NrÝ  r^  z7# Cron job removed without producing output

- job id: r‡   z	
- name: r±   z
- dispatch claimed: rÂ   r«   r‹   rð  z
- run claimed at: Úatr¯   z by Úbyz
- removed at: u   

This one-shot job's dispatch was claimed, but the run never completed (`last_run_at` was never written) â€” the scheduler process was most likely killed or restarted mid-execution. The job has been removed to stop it re-firing; recreate it to run again.
rJ   uX   Job '%s': removed without a completed run â€” diagnostic written to its output directoryz8Failed to write wedged-oneshot diagnostic for job %r: %s)r4   r  r  Úsave_job_outputr_   r`   r^   rJ  )rœ   rÝ  Úclaimr�   r…   s        r   Ú _write_wedged_oneshot_diagnosticrf  µ  sí  € ð ‡w‚wˆ}ÑÔÐ)Øˆð
Ø—’˜Ñ"Ô"Ð( bˆØ—’˜Ñ$Ô$Ð*¨ˆð
ØŸš ™œð
ð 
à—w’w˜v‘”ð
ð 
ð $*§:¢:¨k¸3Ñ#?Ô#?ð
ð 
ð CIÇ*Â*ÈWÐVYÑBZÔBZð
ð 
ð "'§¢¨4°Ñ!;Ô!;ð	
ð 
ð BGÇÂÈ4ÐQZÑA[ÔA[ð	
ð 
õ
 )™]œ]×4Ò4Ñ6Ô6ð
ð 
ð 
ð 	õ 	˜Ÿš  bÑ)Ô)¨4Ñ0Ô0Ð0Ý�Šð#à�GŠG�F˜CŸGšG D¨#Ñ.Ô.Ñ/Ô/ñ	
ô 	
ð 	
ð 	
ð 	
øõ
 ð 
ð 
ð 
Ý�ŠØFØ�GŠG�D‰MŒM˜1ñ	
ô 	
ð 	
ð 	
ð 	
ð 	
ð 	
ð 	
ð 	
øøøøð
øøøs   ™EE Å
F Å&/FÆF c                 ó:  — t          ¦   «         5  t          ¦   «         }t          |¦  «        D �]8\  }}|d         | k    rŒ|                     di ¦  «                             d¦  «        dk    r ddd¦  «         dS |                     d¦  «        }|s ddd¦  «         dS |                     d¦  «        }|�|d	k    r ddd¦  «         dS |                     d
d	¦  «        }||k    �r|                     d¦  «        �qd|d<   d
|d<   d|d<   t	          |¦  «         t
                               d|                     d|                     dd¦  «        ¦  «        ||¦  «          ddd¦  «         dS |                     |¦  «         t	          || h¬¦  «         t          |¦  «         t
                               d|                     d|                     dd¦  «        ¦  «        ||¦  «          ddd¦  «         dS |dz   |d
<   t	          |¦  «         t
           	                    d|                     d|                     dd¦  «        ¦  «        |d
         |¦  «          ddd¦  «         dS t
           	                    d| ¦  «         	 ddd¦  «         dS # 1 swxY w Y   dS )up  Atomically claim a finite one-shot job dispatch BEFORE execution.

    Increments ``repeat.completed`` under the cross-process jobs lock and
    persists the claim immediately, so that if the tick dies mid-execution
    (gateway kill, OOM, segfault, hard-timeout) the dispatch is not lost.
    This converts finite one-shot jobs from *at-least-once* to *at-most-times*
    semantics â€” a job that self-destructs fires at most ``repeat.times`` times
    instead of infinitely (issue #38758).

    Returns ``True`` if the caller may proceed to run the job, ``False`` if the
    dispatch limit is already reached (in which case the stale job is removed).

    Only claims jobs with ``schedule.kind == "once"`` and ``repeat.times > 0``.
    Recurring jobs (they use ``advance_next_run``) and infinite-repeat / no-repeat
    jobs are left unchanged and always allowed to proceed.
    r‡   r§   rð   rû   NTrÝ  rð  r   rÂ   r  Fr¿   rµ   ró  u>   Job '%s': dispatch limit reached (%d/%d) â€” marking completedr±   r«   r�  u5   Job '%s': dispatch limit reached (%d/%d) â€” removingrf   z Job '%s': claimed dispatch %d/%du|   claim_dispatch: job_id %s not in store â€” proceeding without claim (handed-in job dict; nothing to persist a claim against))
r†   ru  r-  r4   rp  r_   ÚinforP  rf  rJ  )rX   rl  r†  rœ   rÝ  rð  rÂ   s          r   Úclaim_dispatchri  á  s¥  € õ" 
‰Œð >ð >Ý‰{Œ{ˆÝ ‘o”oð 5	ñ 5	‰FˆAˆsØ�4Œy˜FÒ"Ð"ØØ�wŠw�z 2Ñ&Ô&×*Ò*¨6Ñ2Ô2°fÒ<Ð<Øð>ð >ð >ñ >ô >ð >ð >ð >ð —W’W˜XÑ&Ô&ˆFØð Øð>ð >ð >ñ >ô >ð >ð >ð >ð —J’J˜wÑ'Ô'ˆEØˆ} ¨¢
 
Øð>ð >ð >ñ >ô >ð >ð >ð >ð Ÿ
š
 ;°Ñ2Ô2ˆIØ˜EÒ!Ñ!à—7’7˜=Ñ)Ô)Ð5ð
 &+�C˜	‘NØ#.�C˜‘LØ)-�C˜Ñ&Ý˜d‘O”O�OÝ—K’KØXØŸš ¨¯ª°°cÑ(:Ô(:Ñ;Ô;Ø!Øñ	ô ð ð !ð?>ð >ð >ñ >ô >ð >ð >ð >ðH —’˜‘”�Ý˜$¨V¨HÐ5Ñ5Ô5Ð5Ý0°Ñ5Ô5Ð5Ý—’ØKØ—G’G˜F C§G¢G¨D°#Ñ$6Ô$6Ñ7Ô7ØØñ	ô ð ð ð[>ð >ð >ñ >ô >ð >ð >ð >ð^ #,¨a¡-ˆF�;ÑÝ�d‰OŒOˆOÝ�LŠLØ2Ø—’˜ §¢¨¨cÑ 2Ô 2Ñ3Ô3Ø�{Ô#Øñ	ô ð ð ðo>ð >ð >ñ >ô >ð >ð >ð >õr 	�ŠðGàñ	
ô 	
ð 	
ð
 ð}>ð >ð >ñ >ô >ð >ð >ð >ð >ð >ð >ð >øøøð >ð >ð >ð >ð >ð >s9   �AJÁ;JÂ JÃBJÅ.A<JÇ7A#JÉ'JÊJÊJÚexpected_ownerc                óD  — t          ¦   «         5  t          ¦   «         }|D ]ç}|                     d¦  «        | k    rŒ|                     di ¦  «                             d¦  «        dk    r ddd¦  «         dS |                     d¦  «        }t          |t          ¦  «        r|                     d¦  «        |k    r ddd¦  «         dS t          ¦   «                              ¦   «         |d	<   t          |¦  «          ddd¦  «         d
S 	 ddd¦  «         n# 1 swxY w Y   dS )a4  Refresh a one-shot's ``run_claim`` timestamp while its run is alive.

    Called periodically from the scheduler's run monitor (#62002) so a
    legitimately long run keeps its claim fresh: an expired claim then really
    does mean "the claiming process died", and neither another process's tick
    nor this process's own next tick will re-dispatch or stale-remove the job
    while the run is in flight. mark_job_run() clears the claim on completion.

    ``expected_owner`` is the stable owner copied from the dispatched job. The
    compare-and-refresh prevents a stale runner that resumes after a long sleep
    from extending a claim another scheduler process has since taken over.

    Returns True if this owner's one-shot claim was refreshed; False when the
    job, claim, or ownership no longer matches.
    r‡   r§   rð   rû   NFr^  rc  rb  T)r†   ru  r4   r•   rž   r  r  rp  )rX   rj  rl  rœ   re  s        r   Úheartbeat_run_claimrl  3	  s¦  € õ  
‰Œð ð Ý‰{Œ{ˆØð 
	ð 
	ˆCØ�wŠw�t‰}Œ} Ò&Ð&ØØ�wŠw�z 2Ñ&Ô&×*Ò*¨6Ñ2Ô2°fÒ<Ð<Øðð ð ñ ô ð ð ð ð —G’G˜KÑ(Ô(ˆEÝ˜e¥TÑ*Ô*ð ¨e¯iªi¸©o¬oÀÒ.OÐ.OØðð ð ñ ô ð ð ð õ &™-œ-×1Ò1Ñ3Ô3ˆE�$‰KÝ�d‰OŒOˆOØðð ð ñ ô ð ð ð ð
	ðð ð ñ ô ð ð ð ð ð ð øøøð ð ð ð ð ˆ5s   �ADÁ7ADÃ3DÄDÄDc                 óð  — t          | ¦  «        }|sdS t          ¦   «         5  t          ¦   «         }t          ¦   «                              ¦   «         }d}|D ]v}|d         |vrŒ|                     di ¦  «                             d¦  «        }|dvrŒ;t          |d         |¦  «        }|r#||                     d¦  «        k    r
||d<   |dz  }Œw|rt          |¦  «         |cddd¦  «         S # 1 swxY w Y   dS )	u  Batch form of :func:`advance_next_run` for the due-dispatch loop.

    One ``load_jobs()`` + at most one ``save_jobs()`` for the whole due
    set, instead of one of each per job â€” the per-job form costs
    O(N loads + N saves) for N due jobs (~110 ms at N=50, measured), the
    batch form O(1 + 1) (~2 ms). ``job_ids`` may contain ids of one-shot
    or unknown jobs; they are skipped exactly as the per-job form skips
    them. Returns the number of jobs whose ``next_run_at`` was advanced.

    Crash semantics: the batch persists once at the end, so a crash
    mid-batch re-fires the whole set on restart (at-least-once burst)
    rather than advancing a prefix â€” acceptable given the sub-10ms window,
    and identical to the per-job form once the batch completes.
    r   r‡   r§   rð   >   r   rï   ró  rf   N)rA   r†   ru  r  r  r4   r/  rp  )Újob_idsr  rl  r   Úadvancedrœ   rð   Únew_nexts           r   Úadvance_next_runsrq  S	  sY  € õ ˆg‰,Œ,€CØð ØˆqÝ	‰Œð ð Ý‰{Œ{ˆÝ‰mŒm×%Ò%Ñ'Ô'ˆØˆØð 		ð 		ˆCØ�4Œy Ð#Ð#ØØ—7’7˜: rÑ*Ô*×.Ò.¨vÑ6Ô6ˆDØÐ/Ð/Ð/ØÝ'¨¨J¬¸Ñ=Ô=ˆHØð ˜H¨¯ª°Ñ(>Ô(>Ò>Ð>Ø%-��MÑ"Ø˜A‘�øØð 	Ý�d‰OŒOˆOØð!ð ð ð ñ ô ð ð ð ð ð ð øøøð ð ð ð ð ð s   ¢B<C+Ã+C/Ã2C/c                 ó*   — t          | g¦  «        dk    S )u  Preemptively advance next_run_at for a recurring job before execution.

    Call this BEFORE run_job() so that if the process crashes mid-execution,
    the job won't re-fire on the next gateway restart.  This converts the
    scheduler from at-least-once to at-most-once for recurring jobs â€” missing
    one run is far better than firing dozens of times in a crash loop.

    One-shot jobs are left unchanged so they can still retry on restart.

    Returns True if next_run_at was advanced, False otherwise.
    rf   )rq  rU  s    r   Úadvance_next_runrs  x	  s   € õ ˜f˜XÑ&Ô&¨!Ò+Ð+r/   c                  óà   — t          j        dd¦  «                             ¦   «         } | r| S 	 ddl}|                     ¦   «         }n# t
          $ r d}Y nw xY w|› dt          j        ¦   «         › �S )zéStable-ish identifier for claim attribution/debugging (NOT correctness).

    Uses ``HERMES_MACHINE_ID`` if set, else hostname + pid. The CAS correctness
    comes from the file lock + the fresh-claim check, not from this value.
    ÚHERMES_MACHINE_IDrJ   r   Nr¯   ú:)rK   rL   rM   ÚsocketÚgethostnamer^   Úgetpid)Úexplicitrw  Úhosts      r   Ú_machine_idr|  ‰	  sŽ   € õ ŒyÐ,¨bÑ1Ô1×7Ò7Ñ9Ô9€HØð ØˆðØˆˆˆØ×!Ò!Ñ#Ô#ˆˆøÝð ð ð ØˆˆˆðøøøàÐ"Ð"•R”Y‘[”[Ð"Ð"Ð"s   ­A ÁAÁAi,  )Úclaim_ttl_secondsr}  c                ó4  — t          ¦   «         5  t          ¦   «         }|D �]]}|d         | k    rŒt          |¦  «        s ddd¦  «         dS t          ¦   «         }|                     d¦  «        }|rn	 t          t          j        |d         ¦  «        ¦  «        }||z
                       ¦   «         }d|cxk    r|k     rn n ddd¦  «         dS n# t          $ r Y nw xY w| 
                    ¦   «         t          ¦   «         dœ|d<   |                     di ¦  «                             d	¦  «        }|d
v r/t          |d         | 
                    ¦   «         ¦  «        }	|	r|	|d<   t          |¦  «          ddd¦  «         dS 	 ddd¦  «         dS # 1 swxY w Y   dS )uT  Atomically claim a job for a single external 'fire' (multi-machine
    at-most-once). Returns True iff THIS caller won the claim.

    Used by the external-provider fire path (``CronScheduler.fire_due``) when an
    external scheduler (Chronos) signals a job is due across N gateway replicas:
    exactly one wins. Single-machine deployments always win.

    Under the file lock: reject if the job is missing/disabled/paused. If a
    fresh claim (younger than ``claim_ttl_seconds``) already exists, lose.
    Otherwise stamp a ``fire_claim`` and, for recurring jobs, advance
    ``next_run_at`` (mirrors ``advance_next_run``'s at-most-once bump so a stale
    re-delivery for the old time can't re-fire). One-shots keep ``next_run_at``
    but the fresh ``fire_claim`` blocks a duplicate retry for the same fire.
    ``mark_job_run`` clears the claim on completion so a re-armed recurring job
    is claimable again next fire.

    The stale-claim TTL means a machine that crashed after claiming but before
    completing doesn't wedge the job forever â€” after the TTL another fire can
    reclaim it.
    r‡   NFr]  rb  r   ©rb  rc  r§   rð   >   r   rï   ró  T)r†   ru  rÀ   r  r4   r  r   r  r#  r^   r  r|  r/  rp  )
rX   r}  rl  rœ   r   ÚexistingÚ
claimed_atÚ_agerð   Únxts
             r   Úclaim_job_for_firer„  š	  s]  € õ* 
‰Œð !ð !Ý‰{Œ{ˆØð 	ñ 	ˆCØ�4Œy˜FÒ"Ð"Øõ # 3Ñ'Ô'ð Øð!ð !ð !ñ !ô !ð !ð !ð !õ ‘-”-ˆCØ—w’w˜|Ñ,Ô,ˆHØð ðÝ!.­xÔ/EÀhÈtÄnÑ/UÔ/UÑ!VÔ!V�Jð   *Ñ,×;Ò;Ñ=Ô=�DØ˜DÐ4Ð4Ò4Ð4Ð#4Ò4Ð4Ð4Ð4Ð4Ø$ð-!ð !ð !ñ !ô !ð !ð !ð !øøõ. !ð ð ð Ø�Dðøøøà'*§}¢}¡¤½k¹m¼mÐ LÐ LˆC�ÑØ—7’7˜: rÑ*Ô*×.Ò.¨vÑ6Ô6ˆDØÐ+Ð+Ð+Ý& s¨:¤¸¿º¹¼ÑHÔH�Øð -Ø),�C˜Ñ&Ý�d‰OŒOˆOØðA!ð !ð !ñ !ô !ð !ð !ð !ðB ðC!ð !ð !ñ !ô !ð !ð !ð !ð !ð !ð !ð !øøøð !ð !ð !ð !ð !ð !sI   �1FÁ%FÁ3ACÃFÃFÃ
CÃFÃCÃBFÅ?FÆFÆFé   c                  ó  — 	 ddl m}   | ¦   «         pi }t          |t          ¦  «        r|                     di ¦  «        ni }t          |                     dt          ¦  «        ¦  «        S # t          $ r t          t          ¦  «        cY S w xY w)a  Resolve the completed one-shot retention window from config.

    ``cron.completed_retention_days`` (number, default
    ``COMPLETED_ONESHOT_RETENTION_DAYS``). A non-positive value disables the
    sweep, retaining completed one-shot records indefinitely.
    r   ©Úload_configr   Úcompleted_retention_days)r·  rˆ  r•   rž   r4   rO   Ú COMPLETED_ONESHOT_RETENTION_DAYSr^   ©rˆ  r»  r¼  s      r   Ú!_completed_oneshot_retention_daysrŒ  Ù	  s«   € ð
7Ø1Ð1Ð1Ð1Ð1Ð1Øˆk‰mŒmÐ!˜rˆÝ*4°S½$Ñ*?Ô*?ÐG�3—7’7˜6 2Ñ&Ô&Ð&ÀRˆÝØ�LŠLØ*Õ,Lñô ñ
ô 
ð 	
øõ
 ð 7ð 7ð 7ÝÕ5Ñ6Ô6Ð6Ð6Ð6ð7øøøs   ‚A&A) Á)B
Â	B
Úraw_jobsc                óÆ  — t          ¦   «         }|dk    rdS |t          |¬¦  «        z
  }d}t          | ¦  «        D �]¤}	 |                     d¦  «        dk    rŒ|                     d¦  «        }t	          |t
          ¦  «        r|                     d¦  «        nd}|d	k    rŒf|                     d
¦  «        }	t	          |	t          ¦  «        sŒ‘	 t          t          j	        |	¦  «        ¦  «        }
n# t          $ r Y ŒÀw xY w|
|k    rŒË|                      |¦  «         d}|                     d¦  «        }|�$|r"|                     t          |¦  «        ¦  «         t                               d|                     d|                     dd¦  «        ¦  «        |	|¦  «         �Œd# t          $ r5 t                               d|                     dd¦  «        d¬¦  «         Y �Œ¢w xY w|S )u{  Prune terminal ``state == "completed"`` one-shot records past retention.

    Mutates *raw_jobs* in place; returns True when anything was removed (the
    caller persists). Ids removed are added to *removed_ids* when provided so
    ``save_jobs``'s shrink-merge guard (#80624) allows the intentional delete.
    Only one-shot (``schedule.kind == "once"``) records in the terminal
    completed state are candidates; recurring jobs and non-terminal one-shots
    are never touched. Age is measured from ``last_run_at`` â€” a completed
    record without a parseable ``last_run_at`` is kept (never guess a record
    into deletion).
    r   F)Údaysrµ   rÂ   r§   rð   Nrû   r  Tr‡   zNJob '%s': pruning completed one-shot record (finished %s, retention %.1f days)r±   r«   z/Retention sweep skipped malformed job record %rr[   )rŒ  r   r–   r4   r•   rž   r�   r  r   r  r^   ÚremoverŽ  r_   rh  rJ  )r�  r   r‚  Úretention_daysÚcutoffÚremovedÚrjr§   rð   Úlast_runÚlast_run_dtÚrids               r   Ú_sweep_completed_oneshotsr˜  í	  s  € õ" 7Ñ8Ô8€NØ˜ÒÐØˆuØ•9 .Ð1Ñ1Ô1Ñ1€FØ€GÝ�8‰nŒnð "ñ "ˆð!	Ø�vŠv�g‰Œ +Ò-Ð-ØØ—v’v˜jÑ)Ô)ˆHÝ+5°hÅÑ+EÔ+EÐO�8—<’< Ñ'Ô'Ð'È4ˆDØ�vŠ~ˆ~ØØ—v’v˜mÑ,Ô,ˆHÝ˜h­Ñ,Ô,ð ØðÝ+­HÔ,BÀ8Ñ,LÔ,LÑMÔM��øÝð ð ð Ø�ðøøøà˜fÒ$Ð$ØØ�OŠO˜BÑÔÐØˆGØ—&’&˜‘,”,ˆCØÐ&¨3Ð&Ø—’¥ C¡¤Ñ)Ô)Ð)Ý�KŠKð5à—’�v˜rŸvšv d¨CÑ0Ô0Ñ1Ô1ØØñô ð ñ øõ ð 	ð 	ð 	Ý�LŠLØAØ—’�t˜SÑ!Ô!Øð ñ ô ð ð ñ ð	øøøð €NsI   ¿FÁAFÂ!*FÃ!C/Ã.FÃ/
C<Ã9FÃ;C<Ã<	FÄBFÆ;GÇGc                  ón   — t          ¦   «         5  t          ¦   «         cddd¦  «         S # 1 swxY w Y   dS )uò  Get all jobs that are due to run now.

    For recurring jobs (cron/interval), if the scheduled time is stale (more
    than one period in the past, e.g. because the gateway was down OR because a
    long-running previous execution overran the interval), the accumulated
    missed runs are collapsed â€” ``next_run_at`` is fast-forwarded to the next
    future occurrence so a backlog does NOT burst-fire on restart â€” but the job
    still fires ONCE now. This prevents the perpetual-defer loop (#33315) where
    a job whose runtime exceeds ``interval + grace`` would be skipped forever.

    Note: firing once on catch-up flows through ``mark_job_run``, so a job with
    a ``repeat.times`` limit consumes one of its runs on that catch-up fire.
    N)r†   Ú_get_due_jobs_lockedr.   r/   r   Úget_due_jobsr›  )
  s{   € õ 
‰Œð &ð &Ý#Ñ%Ô%ð&ð &ð &ð &ñ &ô &ð &ð &ð &ð &ð &ð &øøøð &ð &ð &ð &ð &ð &s   �*ª.±.c                  óÀ  ‡— t          ¦   «         } t          ¦   «         Šd}t          ¦   «         }‰D ]R}|                     d¦  «        s;|                     dd¦  «        pt          j        ¦   «         j        dd…         |d<   d}ŒSd„ t          j	        ‰¦  «        D ¦   «         }g }|D ]1}t          |                     d¦  «        t          ¦  «        si |d<   d}Œ2‰D ]1}t          |                     d¦  «        t          ¦  «        si |d<   d}Œ2|D ]…}|                     d	¦  «        }|�lt          |t          ¦  «        s|                     d	d¦  «         d}ŒG	 t          j        |¦  «         Œ]# t          $ r |                     d	d¦  «         d}Y Œ�w xY wŒ†‰D ]…}|                     d	¦  «        }|�lt          |t          ¦  «        s|                     d	d¦  «         d}ŒG	 t          j        |¦  «         Œ]# t          $ r |                     d	d¦  «         d}Y Œ�w xY wŒ†|D ]š}|                     d
¦  «        }|�.t          |t          ¦  «        s|                     d
d¦  «         d}ŒGt          |t          ¦  «        r>	 t          j        |¦  «         Œr# t          $ r |                     d
d¦  «         d}Y Œ–w xY wŒ›‰D ]š}|                     d
¦  «        }|�.t          |t          ¦  «        s|                     d
d¦  «         d}ŒGt          |t          ¦  «        r>	 t          j        |¦  «         Œr# t          $ r |                     d
d¦  «         d}Y Œ–w xY wŒ›t!          ¦   «         }	t#          ‰| |¬¦  «        rd}ˆfd„|D ¦   «         }|D �]À}
	 |
                     dd¦  «        sŒt%          |
¦  «        r·|
                     d¦  «        }t&                               d|
                     d|¦  «        |¦  «         ‰D ]n}|                     d¦  «        |k    rŒd|d<   d|d<   |                     d¦  «        s|                      ¦   «         |d<   |                     d¦  «        sd|d<   d} Œá|
                     d¦  «        }|rœ|
                     di ¦  «                             d¦  «        dk    ro	 t-          t          j        |d         ¦  «        ¦  «        }| |z
                       ¦   «         }d|cxk    r|	k     rn n�Œvn# t0          t2          t4          f$ r Y nw xY w|
                     d	¦  «        }|sò|
                     di ¦  «        }|                     d¦  «        }t7          || |
                     d
¦  «        ¬¦  «        }|rdnd}|s*|dv r&t9          ||                      ¦   «         ¦  «        }|r|}|s�Œ1||
d	<   |}t&                               d|
                     d|
                     dd¦  «        ¦  «        ||¦  «         ‰D ]}|d         |
d         k    r	||d	<   d} nŒt          j        |¦  «        }|
                     di ¦  «        }|                     d¦  «        }t-          |¦  «        }|dk    rÖ|| k    rÐt=          || ¦  «        rÀt?          || ¦  «        r°t9          ||                      ¦   «         ¦  «        }|rŒt&                               d |
                     d|
                     dd¦  «        ¦  «        |                      ¦   «         |                       ¦   «         |¦  «         ‰D ]}|d         |
d         k    r	||d	<   d} nŒ�ŒÇ|| k    �rœtC          |¦  «        }|dv r³| |z
                       ¦   «         |k    r˜t9          ||                      ¦   «         ¦  «        }|rtt&                               d!|
                     d|
                     dd¦  «        ¦  «        |||¦  «         ‰D ]}|d         |
d         k    r	||d	<   d} nŒtE          ¦   «          |dk    �rl|
                     d"¦  «        }|�rT|                     d#¦  «        }|                     d$d¦  «        }|��&|dk    �r||k    �rtG          |
                     dd%¦  «        ¦  «        rGt&                               d&|
                     d|
                     dd¦  «        ¦  «        ||¦  «         �ŒYt&                               d'|
                     d|
                     dd¦  «        ¦  «        ||¦  «         ‰D ]U}|d         |
d         k    rA‰ $                    |¦  «         | %                    t          |d         ¦  «        ¦  «         d} nŒVtM          |
¦  «         �Œ|dk    rH|                      ¦   «         tO          ¦   «         d(œ}||
d<   ‰D ]}|d         |
d         k    r	||d<   d} nŒ| (                    |
¦  «         �Œl# t          $ rI t&           )                    d)|
                     d¦  «        p|
                     d¦  «        pd¦  «         Y �Œ¾w xY w|rtU          ‰|pd¬¦  «         |S )*zLInner implementation of get_due_jobs(); must be called with _jobs_lock held.Fr‡   rX   Nrå  Tc                 ó,   — g | ]}t          |¦  «        ‘ŒS r.   )rŸ   r‹  s     r   r�  z(_get_due_jobs_locked.<locals>.<listcomp>Q
  s!   € ÐDÐDÐD qÕ Ñ"Ô"ÐDÐDÐDr/   r§   ró  r  r�  c                 óL   •‡— g | ]Št          ˆfd „‰D ¦   «         ¦  «        ¯‰‘Œ S )c              3   óp   •K  — | ]0}|                      d ¦  «        ‰                      d ¦  «        k    V — Œ1dS rŸ  rŠ  )rô   r”  rŒ  s     €r   rö   z2_get_due_jobs_locked.<locals>.<listcomp>.<genexpr>¨
  s<   øè è € Ð&VÐ&VÀr r§v¢v¨d¡|¤|°q·u²u¸T±{´{Ò'BÐ&VÐ&VÐ&VÐ&VÐ&VÐ&Vr/   )r¥  )rô   rŒ  r�  s    @€r   r�  z(_get_due_jobs_locked.<locals>.<listcomp>¨
  s<   øø€ ÐWÐWÐW�a¥3Ð&VÐ&VÐ&VÐ&VÈXÐ&VÑ&VÔ&VÑ#VÔ#VÐW�ÐWÐWÐWr/   r¿   zsJob '%s' (%s) has pause markers while enabled=true; self-disabling so it cannot fire (pause must be authoritative).r±   rº   rµ   r»   rñ  z+auto-disabled: enabled+paused contradictionr^  rð   rû   rb  r   r  zone-shot>   r   rï   z4Job '%s' had no next_run_at; recovering %s run at %sr«   r   zlJob '%s' next_run_at offset changed (%s -> %s). Recomputing cron run to preserve local wall-clock intent: %sz~Job '%s' missed its scheduled time (%s, grace=%ds). Running now; next run provisionally set to: %s (re-anchored on completion)rÝ  rð  rÂ   rJ   ui   Job '%s': dispatch limit reached (%d/%d) but its run is still in flight in this process â€” keeping entryuN   Job '%s': one-shot dispatch limit reached (%d/%d) â€” removing stale due entryr  z.Skipping malformed cron job %r during due scan)+r  ru  rA   r4   rP  r÷  rø  rù  ÚcopyÚdeepcopyr•   rž   r�   r   r  r^   rW   r˜  r½   r_   r}   r  r  r#  ÚKeyErrorrP   rQ   r  r/  rh  r  r  r  r*  rT  ra   r�  rŽ  rf  r|  r—   Ú	exceptionrp  )r   Ú
needs_saveÚintentionally_removedr”  rl  ÚduerŒ  ÚnrÚlrÚ_run_claim_ttlrœ   ÚjidÚexisting_claimr�  r‚  r-  r§   rð   Úrecovered_nextÚrecovery_kindÚraw_next_run_dtÚnext_run_dtrp  r'  rÝ  rð  rÂ   re  r�  s                               @r   rš  rš  ;
  sï  ø€ å
‰-Œ-€CÝ‰{Œ{€HØ€JÝ&)¡e¤eÐð ð ð ˆØ�vŠv�d‰|Œ|ð 	Ø—v’v˜h¨Ñ-Ô-ÐFµ´±´Ô1AÀ#À2À#Ô1FˆBˆt‰HØˆJøàDÐD­D¬M¸(Ñ,CÔ,CÐDÑDÔD€DØ
€Cð ð ð ˆÝ˜!Ÿ%š% 
Ñ+Ô+­TÑ2Ô2ð 	ØˆAˆj‰MØˆJøØð ð ˆÝ˜"Ÿ&š& Ñ,Ô,­dÑ3Ô3ð 	ØˆBˆz‰NØˆJøð ð &ð &ˆØ�UŠU�=Ñ!Ô!ˆØˆ>Ý˜b¥#Ñ&Ô&ð &Ø—’�m TÑ*Ô*Ð*Ø!�
�
ð&ÝÔ*¨2Ñ.Ô.Ð.Ð.øÝ ð &ð &ð &Ø—E’E˜-¨Ñ.Ô.Ð.Ø!%�J�J�Jð&øøøð ð ð &ð &ˆØ�VŠV�MÑ"Ô"ˆØˆ>Ý˜b¥#Ñ&Ô&ð &Ø—’�} dÑ+Ô+Ð+Ø!�
�
ð&ÝÔ*¨2Ñ.Ô.Ð.Ð.øÝ ð &ð &ð &Ø—F’F˜=¨$Ñ/Ô/Ð/Ø!%�J�J�Jð&øøøð ð ð 
"ð 
"ˆØ�UŠU�=Ñ!Ô!ˆØˆ>¥*¨RµÑ"5Ô"5ˆ>Ø�EŠE�- Ñ&Ô&Ð&ØˆJˆJÝ˜�CÑ Ô ð 	"ð"ÝÔ& rÑ*Ô*Ð*Ð*øÝð "ð "ð "Ø—’�m TÑ*Ô*Ð*Ø!�
�
�
ð"øøøð	"ð ð 
"ð 
"ˆØ�VŠV�MÑ"Ô"ˆØˆ>¥*¨RµÑ"5Ô"5ˆ>Ø�FŠF�= $Ñ'Ô'Ð'ØˆJˆJÝ˜�CÑ Ô ð 	"ð"ÝÔ& rÑ*Ô*Ð*Ð*øÝð "ð "ð "Ø—’�} dÑ+Ô+Ð+Ø!�
�
�
ð"øøøð	"õ 4Ñ5Ô5€Nõ ! ¨3Ð<QÐRÑRÔRð XØˆ
ØWÐWÐWÐW˜4ÐWÑWÔWˆàð Fñ Fˆð	Ø—7’7˜9 dÑ+Ô+ð Øõ ! Ñ%Ô%ð Ø—g’g˜d‘m”m�Ý—’ðVà—G’G˜F CÑ(Ô(Øñ	ô ð ð #ð ð �BØ—v’v˜d‘|”| sÒ*Ð*Ø Ø$)�B�y‘MØ"*�B�w‘KØŸ6š6 +Ñ.Ô.ð :Ø*-¯-ª-©/¬/˜˜;™ØŸ6š6 /Ñ2Ô2ð àIð ˜?Ñ+ð "&�JØØð !ŸWšW [Ñ1Ô1ˆNØð  #§'¢'¨*°bÑ"9Ô"9×"=Ò"=¸fÑ"EÔ"EÈÒ"OÐ"OðÝ!.Ý Ô.¨~¸dÔ/CÑDÔDñ"ô "�Jð   *Ñ,×;Ò;Ñ=Ô=�DØ˜DÐ1Ð1Ò1Ð1 >Ò1Ð1Ð1Ð1Ð1Ù øøÝ ¥*­iÐ8ð ð ð Ø�Dðøøøð —w’w˜}Ñ-Ô-ˆHØð %ØŸ7š7 :¨rÑ2Ô2�Ø—|’| FÑ+Ô+�õ "=ØØØ #§¢¨Ñ 6Ô 6ð"ñ "ô "�ð
 /=Ð F 
 
À$�ð &ð -¨$Ð2FÐ*FÐ*FÝ%5°hÀÇÂÁÄÑ%PÔ%P�NØ%ð -Ø(,˜à%ð Ùà%3��MÑ"Ø)�Ý—’ØJØ—G’G˜F C§G¢G¨D°#Ñ$6Ô$6Ñ7Ô7Ø!Ø"ñ	ô ð ð #ð ð �BØ˜$”x 3 t¤9Ò,Ð,Ø,:˜˜=Ñ)Ø%)˜
Ø˜ð -õ
 'Ô4°XÑ>Ô>ˆOØ—w’w˜z¨2Ñ.Ô.ˆHØ—<’< Ñ'Ô'ˆDå'¨Ñ8Ô8ˆKð" ˜’�Ø 3Ò&Ð&Ý-¨o¸sÑCÔCð 'å0°À#ÑFÔFð 'õ ,¨H°c·m²m±o´oÑFÔF�Øð Ý—K’KðWàŸš ¨¯ª°°cÑ(:Ô(:Ñ;Ô;Ø'×1Ò1Ñ3Ô3ØŸš™œØ ñô ð ð 'ð "ð "˜Ø˜dœ8 s¨4¤yÒ0Ð0Ø08˜B˜}Ñ-Ø)-˜JØ!˜Eð 1ñ à˜cÒ!Ñ!õ
 /¨xÑ8Ô8�ØÐ/Ð/Ð/°S¸;Ñ5F×4UÒ4UÑ4WÔ4WÐZ_Ò4_Ð4_õ  0°¸#¿-º-¹/¼/ÑJÔJ�HØð 5ÝŸšð:ð  ŸGšG F¨C¯GªG°D¸#Ñ,>Ô,>Ñ?Ô?Ø$Ø!Ø$ñô ð ð" #+ð &ð &˜BØ! $œx¨3¨t¬9Ò4Ð4Ø4<  =Ñ 1Ø-1 
Ø % ð  5õ 3Ñ4Ô4Ð4ð ˜6’>‘>Ø ŸWšW XÑ.Ô.�FØñ )%Ø &§
¢
¨7Ñ 3Ô 3˜Ø$*§J¢J¨{¸AÑ$>Ô$>˜	Ø Ñ,°¸²±¸yÈEÒ?QÑ?Qõ  <¸C¿GºGÀDÈ"Ñ<MÔ<MÑNÔNð 	)Ý &§¢ð%@ð %(§G¢G¨F°C·G²G¸DÀ#Ñ4FÔ4FÑ$GÔ$GØ$-Ø$)ñ!"ô !"ð !"ñ !)Ý"ŸKšKð!?à #§¢¨°·²¸¸cÑ0BÔ0BÑ CÔ CØ )Ø %ñô ð ð '/ð *ð * Ø#% d¤8¨s°4¬yÒ#8Ð#8Ø$,§O¢O°BÑ$7Ô$7Ð$7Ø$9×$=Ò$=½cÀ"ÀTÄ(¹m¼mÑ$LÔ$LÐ$LØ15 JØ$) Eð	 $9õ =¸SÑAÔAÐAÙ$ð$ ˜6’>�>Ø#&§=¢=¡?¤?½+¹-¼-ÐHÐH�EØ',�C˜Ñ$Ø&ð "ð "˜Ø˜dœ8 s¨4¤yÒ0Ð0Ø.3˜B˜{™OØ)-˜JØ!˜Eð 1ð
 —
’
˜3‘”�ùøÝð 	ð 	ð 	Ý×ÒØ@Ø—’˜‘”Ð7 3§7¢7¨4¡=¤=Ð7°Cñô ð ð ‰Hð	øøøð ð GÝ�(Ð(=Ð(EÀÐFÑFÔFÐFà€Js«   ÅE*Å*"FÆFÇG2Ç2"HÈHÉ:JÊ"J4Ê3J4ÌL,Ì,"MÍMÎg3Î"Cg3Ñ(Ag3Ò-AS>Ó=g3Ó>TÔg3ÔTÔBg3Ö8Fg3ÝFg3ã B,g3æA#g3ç3Aiéir´   c                  ó   — 	 ddl m}   | ¦   «         pi }t          |t          ¦  «        r|                     di ¦  «        ni }t          |                     dt          ¦  «        ¦  «        S # t          $ r
 t          cY S w xY w)zVResolve the per-job output-file retention cap from config (``cron.output_retention``).r   r‡  r   Úoutput_retention)r·  rˆ  r•   rž   r4   rè   Ú_CRON_OUTPUT_DEFAULT_KEEPr^   r‹  s      r   Ú_cron_output_keepr³  À  s•   € ð)Ø1Ð1Ð1Ð1Ð1Ð1Øˆk‰mŒmÐ!˜rˆÝ*4°S½$Ñ*?Ô*?ÐG�3—7’7˜6 2Ñ&Ô&Ð&ÀRˆÝ�8—<’<Ð 2Õ4MÑNÔNÑOÔOÐOøÝð )ð )ð )Ý(Ð(Ð(Ð(ð)øøøs   ‚A&A) Á)A=Á<A=rL  Úkeepc                 ó^  — |dk    rdS 	 t          d„ |                      d¦  «        D ¦   «         d„ d¬¦  «        }n# t          $ r Y dS w xY wd}||d…         D ]T}	 |                     ¦   «          |dz  }Œ# t          $ r+}t                               d	|j        |¦  «         Y d}~ŒMd}~ww xY w|S )
aÎ  Remove the oldest ``*.md`` run-output files beyond *keep*. Returns count deleted.

    Mirrors the quick-snapshot retention in ``hermes_cli.backup._prune_quick_snapshots``:
    output filenames are timestamp-based (``%Y-%m-%d_%H-%M-%S.md``) so a reverse
    lexical sort orders newest-first, and everything past *keep* is the tail to
    drop. A non-positive *keep* disables pruning. Pruning failures are swallowed
    so they can never break output saving.
    r   c              3   óB   K  — | ]}|                      ¦   «         ¯|V — Œd S ©N)Úis_file)rô   rE  s     r   rö   z$_prune_job_output.<locals>.<genexpr>Ø  s/   è è € ÐCÐC�1°q·y²y±{´{ÐCˆQÐCÐCÐCÐCÐCÐCr/   z*.mdc                 ó   — | j         S r·  )r±   )rE  s    r   ú<lambda>z#_prune_job_output.<locals>.<lambda>Ù  s   € ˜!œ&€ r/   T)r¬   ÚreverseNrf   z"Failed to prune cron output %s: %s)r,  Úglobr{   rB  r_   rJ  r±   )rL  r´  ÚfilesÚdeletedÚstaleÚexcs         r   Ú_prune_job_outputrÁ  Ë  s  € ð ˆq‚y€yØˆqðÝØCÐC˜×+Ò+¨FÑ3Ô3ÐCÑCÔCØ Ð Øð
ñ 
ô 
ˆˆøõ
 ð ð ð Øˆqˆqðøøøà€GØ�t�u�u”ð Pð Pˆð	PØ�LŠL‰NŒNˆNØ�q‰LˆGˆGøÝð 	Pð 	Pð 	PÝ�LŠLÐ=¸u¼zÈ3ÑOÔOÐOÐOÐOÐOÐOÐOøøøøð	Pøøøà€Ns'   Š0; »
A	ÁA	ÁA5Á5
B*Á?!B%Â%B*c                 ó  — t          ¦   «          t          | ¦  «        }|                     dd¬¦  «         t          |¦  «         t	          ¦   «                              d¦  «        }||› d�z  }t          j        t          |¦  «        dd¬¦  «        \  }}	 t          j
        |dd	¬
¦  «        5 }|                     |¦  «         |                     ¦   «          t          j        |                     ¦   «         ¦  «         ddd¦  «         n# 1 swxY w Y   t          ||¦  «         t!          |¦  «         n5# t"          $ r( 	 t          j        |¦  «         n# t&          $ r Y nw xY w‚ w xY wt)          |t+          ¦   «         ¦  «         |S )zSave job output to file.TrÙ   z%Y-%m-%d_%H-%M-%Sz.mdr4  z.output_r5  r9  rg   rh   N)rq   r‘   rÜ   rÊ   r  r  r:  r;  r�   rK   r=  r>  r?  r@  r�   r   rÍ   rA  rB  r{   rÁ  r³  )rX   r"   rL  Ú	timestampÚoutput_filerC  rD  rE  s           r   rd  rd  è  sÈ  € å�M„M€MÝ$ VÑ,Ô,€NØ×Ò °ÐÑ5Ô5Ð5Ý�ÑÔÐå‘”×&Ò&Ð':Ñ;Ô;€IØ  iÐ#4Ð#4Ð#4Ñ4€KåÔ#­¨NÑ(;Ô(;ÀFÐS]Ð^Ñ^Ô^�L€BˆðÝŒY�r˜3¨Ð1Ñ1Ô1ð 	!°QØ�GŠG�F‰OŒOˆOØ�GŠG‰IŒIˆIÝŒH�Q—X’X‘Z”ZÑ Ô Ð ð	!ð 	!ð 	!ñ 	!ô 	!ð 	!ð 	!ð 	!ð 	!ð 	!ð 	!øøøð 	!ð 	!ð 	!ð 	!õ 	�x Ñ-Ô-Ð-Ý�[Ñ!Ô!Ð!Ð!øÝð ð ð ð	ÝŒI�hÑÔÐÐøÝð 	ð 	ð 	ØˆDð	øøøàðøøøõ �nÕ&7Ñ&9Ô&9Ñ:Ô:Ð:àÐsU   ÂD3 Â,ADÃ<D3 ÄDÄD3 ÄDÄ"D3 Ä3
E%Ä>EÅE%Å
E ÅE%ÅE Å E%rU   c                 ó"  — t          | pd¦  «                             ¦   «         }|sdS 	 ddlm}  ||¦  «        p|}n-# t          $ r  t
                               d| d¬¦  «         Y nw xY w|                     ¦   «                              d¦  «        S )u‚  Reduce one job skill reference to the bare name the curator matches on.

    A job may store an absolute path under ``HERMES_HOME/skills`` or an
    external skills dir; the scheduler resolves those through
    ``normalize_skill_lookup_name`` before handing them to ``skill_view``.
    The curator compares this set against bare skill names, so it has to
    resolve them the same way â€” otherwise a path-referencing job's skill
    looks unreferenced and gets archived out from under it.

    Best-effort: if the resolver is unavailable or rejects the value, fall
    back to the plain cleanup so a broken import can never lose a name.
    rJ   r   )Únormalize_skill_lookup_namez8referenced_skill_names: could not normalize skill ref %rTr[   r‹   )r�   rM   Úagent.skill_utilsrÆ  r^   r_   rJ  Úlstrip)rU   r    rÆ  s      r   Ú_canonical_skill_refrÉ    sÀ   € õ ��	�r‰NŒN× Ò Ñ"Ô"€EØð Øˆrð
ØAÐAÐAÐAÐAÐAØ+Ð+¨EÑ2Ô2Ð;°eˆˆøÝð 
ð 
ð 
Ý�ŠØFÈØð 	ñ 	
ô 	
ð 	
ð 	
ð 	
ð
øøøð
 �;Š;‰=Œ=×Ò Ñ$Ô$Ð$s   ©= ½'A'Á&A'c                  óª  — 	 t          ¦   «         } n:# t          $ r- t                               dd¬¦  «         t	          ¦   «         cY S w xY wt	          ¦   «         }| D ]w}t          |t          ¦  «        sŒt          |                     d¦  «        |                     d¦  «        ¦  «        D ](}t          |¦  «        }|r| 
                    |¦  «         Œ)Œx|S )uÏ  Return the set of skill names referenced by ANY cron job.

    Includes paused and disabled jobs deliberately: a paused job never
    fires, so its skills never get a ``bump_use`` from the scheduler, yet
    resuming it must still find its skills present. The curator uses this
    set to protect referenced skills from inactivity archival â€” a skill a
    live job depends on is "in use" regardless of when it was last loaded.

    Names are canonicalized the way the scheduler resolves them at load
    time, so a job that stores an absolute skill path is protected too.

    Best-effort: a corrupt/unreadable jobs store returns an empty set
    rather than raising, so a cron issue can never break the curator.
    z0referenced_skill_names: failed to load cron jobsTr[   r’   r“   )ru  r^   r_   rJ  rA   r•   rž   r›   r4   rÉ  rŽ  )rl  Únamesrœ   r±   Úcleaneds        r   Úreferenced_skill_namesrÍ  &  sæ   € ðÝ‰{Œ{ˆˆøÝð ð ð Ý�ŠÐGÐRVˆÑWÔWÐWÝ‰uŒuˆˆˆðøøøõ ‘e”e€EØð #ð #ˆÝ˜#�tÑ$Ô$ð 	ØÝ)¨#¯'ª'°'Ñ*:Ô*:¸C¿GºGÀHÑ<MÔ<MÑNÔNð 	#ð 	#ˆDÝ*¨4Ñ0Ô0ˆGØð #Ø—	’	˜'Ñ"Ô"Ð"øð	#ð €Ls   ‚ ‘4AÁAÚconsolidatedÚprunedc                 ó6  — t          | pi ¦  «        } t          |pg ¦  «        }|t          |                      ¦   «         ¦  «        z  }| s|sg dddœS t          ¦   «         5  t	          ¦   «         }g }d}|D �]3}t          |                     d¦  «        |                     d¦  «        ¦  «        }|sŒ<i }g }	g }
|D ]b}|| v r)| |         }|||<   |r||
vr|
                     |¦  «         Œ/||v r|	                     |¦  «         ŒI||
vr|
                     |¦  «         Œc|s|	sŒ¬|
|d<   |
r|
d         nd|d<   d}|                     |                     d¦  «        |                     d	¦  «        p|                     d¦  «        t          |¦  «        t          |
¦  «        ||	d
œ¦  «         �Œ5|r7t          |¦  «         t                               dt          |¦  «        ¦  «         |t          |¦  «        t          |¦  «        dœcddd¦  «         S # 1 swxY w Y   dS )u	  Rewrite cron job skill references after a curator consolidation pass.

    When the curator consolidates a skill X into umbrella Y (or archives X
    as pruned), any cron job that lists ``X`` in its ``skills`` field will
    fail to load ``X`` at run time â€” the scheduler logs a warning and
    skips the skill, so the job runs without the instructions it was
    scheduled to follow. See cron/scheduler.py where ``skill_view`` is
    called per skill name.

    This function repairs cron jobs in-place:

    - A skill listed in ``consolidated`` is replaced with its umbrella
      target (the ``into`` value). If the umbrella is already in the
      job's skill list, the stale name is dropped without duplication.
    - A skill listed in ``pruned`` is dropped outright â€” there is no
      forwarding target.
    - Ordering and other skills in the list are preserved.
    - The legacy ``skill`` field is realigned via ``_apply_skill_fields``.

    Args:
        consolidated: mapping of ``old_skill_name -> umbrella_skill_name``.
        pruned: list of skill names that were archived with no forwarding
            target.

    Returns a report dict::

        {
            "rewrites": [
                {
                    "job_id": ...,
                    "job_name": ...,
                    "before": [...],
                    "after": [...],
                    "mapped": {"old": "new", ...},
                    "dropped": ["old", ...],
                },
                ...
            ],
            "jobs_updated": N,
            "jobs_scanned": M,
        }

    Best-effort: exceptions from loading/saving propagate to the caller so
    tests can assert behaviour; the curator invocation site wraps this
    call in a try/except so a failure here never breaks the curator.
    r   )ÚrewritesÚjobs_updatedÚjobs_scannedFr’   r“   NTr‡   r±   )rX   Újob_namerÎ   ÚafterÚmappedÚdroppedz2Curator rewrote skill references in %d cron job(s))rž   rA   Úkeysr†   ru  r›   r4   r—   r–   rp  r_   rh  rÿ   )rÎ  rÏ  Ú
pruned_setrl  rÑ  Úchangedrœ   Úskills_beforerÖ  r×  Ú
new_skillsr±   Útargets                r   Úrewrite_skill_refsrÞ  F  sÆ  € õd ˜Ð*¨Ñ+Ô+€LÝ�V�\˜rÑ"Ô"€Jð •#�l×'Ò'Ñ)Ô)Ñ*Ô*Ñ*€Jàð F 
ð FØ°À1ÐEÐEÐEå	‰Œð 3
ð 3
Ý‰{Œ{ˆØ)+ˆØˆàð "	ñ "	ˆCÝ1°#·'²'¸'Ñ2BÔ2BÀCÇGÂGÈHÑDUÔDUÑVÔVˆMØ ð Øà%'ˆFØ!#ˆGØ$&ˆJà%ð 	,ð 	,�Ø˜<Ð'Ð'Ø)¨$Ô/�FØ#)�F˜4‘LØð 2 &°
Ð":Ð":Ø"×)Ò)¨&Ñ1Ô1Ð1øØ˜ZÐ'Ð'Ø—N’N 4Ñ(Ô(Ð(Ð(Ø Ð+Ð+Ø×%Ò% dÑ+Ô+Ð+øàð  'ð Øà&ˆC�‰MØ,6Ð@˜: aœ=˜=¸DˆC�‰LØˆGà�OŠOØŸ'š' $™-œ-ØŸGšG F™OœOÐ<¨s¯wªw°t©}¬}Ý˜}Ñ-Ô-Ý˜jÑ)Ô)Ø Ø"ðð ñ ô ð ñ ð ð 	Ý�d‰OŒOˆOÝ�KŠKØDÅcÈ(ÁmÄmñô ð ð
 !Ý ™MœMÝ ™IœIð
ð 
ð_3
ð 3
ð 3
ð 3
ñ 3
ô 3
ð 3
ð 3
ð 3
ð 3
ð 3
ð 3
øøøð 3
ð 3
ð 3
ð 3
ð 3
ð 3
s   ÁF"HÈHÈHrÉ  )rJ   r·  )F)r   N)NNNNNNNNNNNNNFNNN)NNN)£r  Ú
contextlibr   Úcontextvarsr   Údataclassesr   rg  ÚloggingrG  r:  Ú	threadingru   rK   ræ   r÷  rt   r   r€   r   r   Úpathlibr   Úhermes_constantsr   Útypingr	   r
   r   r   r   r   r   r   Ú	getLoggerr*   r_   Úhermes_timer   r  Úutilsr   r   r   r   r   r-   r   r9   r:   r5   r6   ÚTICKER_HEARTBEAT_FILEÚTICKER_SUCCESS_FILEÚTICKER_INTERVAL_SECONDSÚRLockro   rä  rn   rw   r7   r  r&   r2   r8   r>   Úcontextmanagerr�   rD   rF   rR   rT   rN   rO   rW   ra   rc   r†   Ú	frozensetr*  r‘   r›   rŸ   r¤   r­   r¸   r½   rÀ   r¶   rÊ   rÍ   Ústat_resultr×   rq   rè   rì   r  r  r  r  r  rž   r*  r/  r2  rF  rI  rL  rO  rQ  rT  rY  r[  r]  rb  rj  ru  rx  rm  rn  r•  rª  rp  r±  r¿  rÄ  rÖ  rØ  rÜ  r  r  ÚLookupErrorr	  r  r&  r9  r=  r@  rB  rM  rR  rV  rX  r`  rf  ri  rl  rq  rs  r|  r„  rŠ  rŒ  r˜  r›  rš  r²  r³  rÁ  rd  rÉ  rÍ  rÞ  r.   r/   r   ú<module>rò     sï  ððð ð ð Ð Ð Ð Ø €€€Ø "Ð "Ð "Ð "Ð "Ð "Ø !Ð !Ð !Ð !Ð !Ð !Ø €€€Ø €€€Ø €€€Ø €€€Ø Ð Ð Ð Ø €€€Ø 	€	€	€	Ø 	€	€	€	Ø €€€ðØ€L€L€L€LøØð ð ð Ø€E€E€EðøøøðØ€M€M€M€MøØð ð ð Ø€F€F€Fðøøøà (Ð (Ð (Ð (Ð (Ð (Ð (Ð (Ø Ð Ð Ð Ð Ð Ø ,Ð ,Ð ,Ð ,Ð ,Ð ,Ø KÐ KÐ KÐ KÐ KÐ KÐ KÐ KÐ KÐ KÐ KÐ KÐ KÐ KÐ KÐ KÐ KÐ KÐ KÐ Kà	ˆÔ	˜8Ñ	$Ô	$€à *Ð *Ð *Ð *Ð *Ð *Ø 3Ð 3Ð 3Ð 3Ð 3Ð 3Ð 3Ð 3ð €Ø#€ˆh�tŒnÐ #Ð #Ñ #ð
˜$ð 
ð 
ð 
ð 
ð8 ˆ_ÑÔ×&Ò&Ñ(Ô(€
ð ˜Ñ€Ø�{Ñ"€	ð !Ð#5Ñ5Ð ð Ð!6Ñ6Ð ð
 Ð ð
 "�)”/Ñ#Ô#€Ø"�9”?Ñ$Ô$Ð ð "Ð Ø˜Ñ €
ØÐ ð €�$ÐÑÔðð ð ð ð ñ ô ñ Ôðð ?I¸jØØð?ñ ?ô ?Ð �j ¨/Ô!:Ô;ð ð ñ ð  � ¨)°ZÑ@Ô@€ðR˜_ð Rð Rð Rð Rð> Ôð*˜˜s D˜yÔ)ð *ð *ð *ñ Ôð*ð ,˜Tð ,ð ,ð ,ð ,ð !%Ð ð #$Ð à#(Ð  ð¨ð ð ð ð ð:¨ð °ð ð ð ð ð49˜ð 9ð 9ð 9ð 9ð
 Ôðe/ð e/ñ Ôðe/ðV "˜	 4 &Ñ)Ô)Ð ð3˜Cð 3 Dð 3ð 3ð 3ð 3ð ð  ¨#¤ð ¸xÈ¼}ð ÐX\Ð]`ÔXað ð ð ð ð"˜T # s (œ^ð °°S¸#°X´ð ð ð ð ðð ˜Cð ¨3ð ¸ð ð ð ð ð 4¨¨S¨¤>ð °cð ð ð ð ð" ˜t C¨ Hœ~ð  °$°s¸C°x´.ð  ð  ð  ð  ðF&˜4  S œ>ð &¨dð &ð &ð &ð &ð˜˜c 3˜hœð ¨Dð ð ð ð ð!˜T # s (œ^ð !°ð !ð !ð !ð !ð(�dð ð ð ð ð�tð ð ð ð ð#
 4ð #
°¸"¼.Ô1Ið #
Èdð #
ð #
ð #
ð #
ðL"ð "ð "ð%�cð %˜cð %ð %ð %ð %ð*a˜Sð a T¨#¨s¨(¤^ð að að að aðH$�hð $ 8ð $ð $ð $ð $ð&	5 hð 	5¸ð 	5Àdð 	5ð 	5ð 	5ð 	5ð
F¨ð 
F¸Hð 
FÈð 
Fð 
Fð 
Fð 
Fð" "&ð	ð ð Ø�3˜�8Œnðà	ðð ˜#”ð	ð
 ˆc„]ðð ð ð ð< Tð ¨cð ð ð ð ðD;ð ;˜t C¨ Hœ~ð ;¸HÀS¼Mð ;ÐU]Ð^aÔUbð ;ð ;ð ;ð ;ðD	A˜dð 	A tð 	Að 	Að 	Að 	Að ð ¨Sð °Tð ð ð ð ð$ð  Tð °dð ð ð ð ð8˜$ð  8¨E¤?ð ð ð ð ð@ (¨5¤/ð @ð @ð @ð @ðC ¨¤ð Cð Cð Cð Cð
ð 
ð 
ð 
ð  ð  ¨ð  ð  ð  ð  ðF sð ð ð ð ðð ð ð ð˜x¨œ}ð ð ð ð ð$3 ð 3¨¨s°D¨yÔ)9ð 3ð 3ð 3ð 3ð",�4˜˜S #˜XœÔ'ð ,ð ,ð ,ð ,ð^˜X d¨4°°S°¬>Ô&:Ô;ð ð ð ð ð2 ð ¨°%¸¸SÀ#¸Ô2FÔ)Gð ð ð ð ð&(˜h u¨S°#°s¨]Ô';Ô<ð (Àð (ð (ð (ð (ð( .2ð<ð <ð <Ø
ˆt�C˜�HŒ~Ô
ð<ð ˜* Sœ/Ô*ð<ð 
ˆ$ˆs�CˆxŒ.Ôð	<ð <ð <ð <ðD .2Øð	Cð Cð CØ
ˆt�C˜�HŒ~Ô
ðCð ˜* Sœ/Ô*ðCð ð	Cð Cð Cð CðR .2Øð	Lð Lð LØ
ˆt�C˜�HŒ~Ô
ðLð ˜* Sœ/Ô*ðLð ð	Lð Lð Lð Lð ¨¤ð °(¸3´-ð ð ð ð ðB)¨°#¬ð )ð )ð )ð )ðX NSð ð ð ¨ð Àdð ÐW_Ð`cÔWdð ð ð ð ð*-àð*-ð ð*-ð ð	*-ð
 ð*-ð ˆ8�CŒ=˜( 3œ-Ð'Ô(ð*-ð *-ð *-ð *-ðZ D¨¨c¨¤Nð °u¸XÀc¼]ÈHÐUXÌMÐ[cÐdgÔ[hÐjnÐ=nÔ7oð ð ð ð ð
Ø˜S”Mð
à˜#”ð
ð ð
ð �SŒMð	
ð
 
ð
ð 
ð 
ð 
ðB Ø Ø!Ø'+ØØ"&ØØ"Ø"Ø Ø48Ø,0Ø!ØØ(,Ø$(Ø!%ð'_ð _Ø�SŒMð_àð_ð �3Œ-ð_ð �SŒMð	_ð
 �cŒ]ð_ð �T˜#˜s˜(”^Ô$ð_ð �CŒ=ð_ð �T˜#”YÔð_ð �CŒ=ð_ð �sŒmð_ð �sŒmð_ð �SŒMð_ð ˜5  d¨3¤i Ô0Ô1ð_ð ˜t CœyÔ)ð_ð �cŒ]ð_ð  ð!_ð"   ”~ð#_ð$ ˜S”Mð%_ð& ˜#”ð'_ð( 
ˆ#ˆsˆ(„^ð)_ð _ð _ð _ðD�Cð ˜H T¨#¨s¨(¤^Ô4ð ð ð ð ð

ð 

ð 

ð 

ð 

˜Kñ 

ô 

ð 

ð2˜ð 2 ¨$¨s°C¨x¬.Ô!9ð 2ð 2ð 2ð 2ð2ð  ð °°d¸3À¸8´nÔ1Eð ð ð ð ð {�sð { T¨#¨s¨(¤^ð {¸ÀÀcÈ3ÀhÄÔ8Pð {ð {ð {ð {ð|ð �cð  8¨C¤=ð ¸HÀTÈ#ÈsÈ(Ä^Ô<Tð ð ð ð ð �sð ˜x¨¨S°#¨X¬Ô7ð ð ð ð ð2˜ð  ¨¨c°3¨h¬Ô 8ð ð ð ð ð"�sð ˜tð ð ð ð ðD 3ð ¨tð ¸ð ð ð ð ð20 3ð 0¨4ð 0ð 0ð 0ð 0ð
* Cð *¨Dð *ð *ð *ð *ð
 EIØ15Ø)-ðsSð sS˜ð sS tð sS°H¸S´Mð sSØ!)¨#¤ðsSà! #œðsSð sSð sSð sSðl)
¨$¨s°C¨x¬.ð )
¸Tð )
ð )
ð )
ð )
ðXO˜3ð O 4ð Oð Oð Oð Oðd ð ¸ð Àð ð ð ð ð@" #ð "ð "ð "ð "ðJ,˜Sð , Tð ,ð ,ð ,ð ,ð"#�Sð #ð #ð #ð #ð" ADð 6ð 6ð 6˜sð 6¸#ð 6Èð 6ð 6ð 6ð 6ðx $%Ð  ð7¨5ð 7ð 7ð 7ð 7ð0 '+ð	9ð 9ð 9Ø�4˜˜S˜”>Ô"ð9à	ð9ð ˜#˜cœ(Ô#ð	9ð
 
ð9ð 9ð 9ð 9ðx&�d˜4  S œ>Ô*ð &ð &ð &ð &ð$z˜d 4¨¨S¨¤>Ô2ð zð zð zð zðD Ð ð)˜3ð )ð )ð )ð )ð dð °#ð ¸#ð ð ð ð ð:˜Cð ¨ð ð ð ð ðF%˜cð % cð %ð %ð %ð %ð6  C¤ð ð ð ð ðB .2Ø"&ðn
ð n
Ø˜4  S œ>Ô*ðn
à�T˜#”YÔðn
ð 
ˆ#ˆsˆ(„^ðn
ð n
ð n
ð n
ð n
ð n
s#   ½A ÁAÁAÁA ÁAÁA