
    Jizb                     L   d 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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  ej        e          ZddlmZ 	 ddlmZ d	Zn# e$ r d
ZY nw xY w e            Zedz  Zedz  Zedz  ZdZ dBdee!         dee         dee!         fdZ"dee!ef         dee!ef         fdZ#defdZ$defdZ%d Z&de!de'fdZ(de!dee!ef         fdZ)dedefdZ*dddee!ef         d ed!ee!         dee!         fd"Z+de,de'fd#Z-dCdee!ef         d!ee!         dee!         fd$Z.deee!ef                  fd%Z/d&eee!ef                  fd'Z0	 	 	 	 	 	 	 	 	 dDd(e!de!d)ee!         d*ee'         d+ee!         d,eee!ef                  dee!         deee!                  d-ee!         d.ee!         d/ee!         dee!ef         fd0Z1d1e!deee!ef                  fd2Z2dEd3e3deee!ef                  fd4Z4d1e!d5ee!ef         deee!ef                  fd6Z5dCd1e!d7ee!         deee!ef                  fd8Z6d1e!deee!ef                  fd9Z7d1e!deee!ef                  fd:Z8d1e!de3fd;Z9dCd1e!d<e3d=ee!         fd>Z:d1e!de3fd?Z;deee!ef                  fd@Z<d1e!de!fdAZ=dS )Fz
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)datetime	timedelta)Path)get_hermes_home)OptionalDictListAny)now)croniterTFcronz	jobs.jsonoutputx   skillskillsreturnc                     || 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.N )
isinstancestrliststripappend)r   r   	raw_items
normalizeditemtexts         &/home/ubuntu/hermes-agent/cron/jobs.py_normalize_skill_listr   )   s    ~$,UGG"			FC	 	  !H		LL	J $ $4:2$$&& 	$D
**d###    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   get)r!   r   r   s      r   _apply_skill_fieldsr%   :   s[    cJ":>>'#:#:JNN8<T<TUUF!Jx'-7&))4Jw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)oschmodOSErrorNotImplementedErrorr&   s    r   _secure_dirr-   C   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)existsr(   r)   r*   r+   r,   s    r   _secure_filer0   K   sa    ;;== 	"HT5!!!!!	" 	"()   s   )/ AAc                      t                               dd           t                              dd           t          t                      t          t                     dS )z6Ensure cron directories exist with secure permissions.Tparentsexist_okN)CRON_DIRmkdir
OUTPUT_DIRr-    r    r   ensure_dirsr9   T   sS    NN4$N///TD111
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'      r   <   i  )mhd)r   lowerrematch
ValueErrorintgroup)r:   rD   valueunitmultiplierss        r   parse_durationrK   `   s     	
		AH\^_``E [YqYYYZZZAE;;q>>!D..K;t$$$r    schedulec                 `   |                                  } | }|                                 }|                    d          r5| dd                                          }t          |          }d|d| ddS |                                 }t          |          dk    rut          d |dd         D                       rTt          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        |                                }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minutesdisplay   c              3   @   K   | ]}t          j        d |          V  dS )z^[\d\*\-,/]+$N)rC   rD   ).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   )rP   exprrR   Tz^\d{4}-\d{2}-\d{2}Zz+00:00oncezonce at z%Y-%m-%d %H:%M)rP   run_atrR   zInvalid timestamp 'rQ   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))r   rB   
startswithrK   splitlenallHAS_CRONITERrE   r   	ExceptionrC   rD   r   fromisoformatreplacetzinfo
astimezone	isoformatstrftime_hermes_nowr   )	rL   originalschedule_lowerduration_strrQ   partsedtr\   s	            r   parse_schedulerq   u   s   $ ~~HH^^%%N   ** 
|))++ ..****
 
 	
 NNE
5zzQ3  /4RaRy      	pnooo	KX 	K 	K 	KIIIaIIJJJ	K 
 
 	
 h"(#8(CC	E'(8(8h(G(GHHB y ]]__,,..Ebkk2B&C&CEE  
  	E 	E 	EC8CCCCDDD	E	 **7!;!;!;;&&((,(,,
 
 	

     	CX 	C 	C 	C  sD   C$ $
D.DD+A0F 
G &F;;G AH 
HHrp   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.
    N)rf   )rj   rf   r   r   rg   re   )rp   	target_tzlocal_tzs      r   _ensure_awareru      sf     $I	y<>>,,..5zzz**55i@@@==###r    last_run_atr   rw   c                    |                      d          dk    rdS |rdS |                      d          }|sdS t          t          j        |                    }||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.
    rP   r[   Nr\   )seconds)r$   ru   r   rd   r   ONESHOT_GRACE_SECONDS)rL   r   rw   r\   	run_at_dts        r   _recoverable_oneshot_run_atr|      s     ||Fv%%t t\\(##F th4V<<==IC),ABBBBBB4r    c                 6   d}d}|                      d          }|dk    r<|                      dd          dz  }|dz  }t          |t          ||                    S |d	k    rt          r	 t	                      }t          | d
         |          }|                    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   rP   rO   rQ   r<   r>   r=   r   rX   )r$   maxminrb   rj   r   get_nextr   rF   total_secondsrc   )
rL   	MIN_GRACE	MAX_GRACErP   period_secondsgracer   r   firstseconds
             r   _compute_grace_secondsr      s     II<<Dz!i33b8!#9c%33444v~~,~		--CHV,c22DMM(++E]]8,,F &5.!?!?!A!ABBN"a'Ey#eY"7"7888 	 	 	D	 s   *BD	 	
DDc                    t                      }| d         dk    rt          | ||          S | d         dk    rf| d         }|r5t          t          j        |                    }|t          |          z   }n|t          |          z   }|                                S | d         dk    rMt          sdS t          | d	         |          }|	                    t                    }|                                S dS )
zo
    Compute the next run time for a schedule.

    Returns ISO timestamp string, or None if no more runs.
    rP   r[   rv   rO   rQ   r]   r   NrX   )
rj   r|   ru   r   rd   r   rh   rb   r   r   )rL   rw   r   rQ   lastnext_runr   s          r   compute_next_runr     s    --C6!!*8SkRRRR	&	Z	'	'9% 	8 !7!D!DEEDi8888HH Yw7777H!!###	&	V	#	# 	4(#..==**!!###4r    c                     t                       t                                          sg S 	 t          t          dd          5 } t	          j        |           }|                    dg           cddd           S # 1 swxY w Y   dS # t          j        $ r 	 t          t          dd          5 } t	          j        | 	                                d          }|                    dg           }|r)t          |           t                              d           |cddd           cY S # 1 swxY w Y   Y dS # t          $ r g cY cY S w xY wt          $ r g cY S w xY w)	zLoad all jobs from storage.rutf-8encodingjobsNF)strictz8Auto-repaired jobs.json (had invalid control characters))r9   	JOBS_FILEr/   openjsonloadr$   JSONDecodeErrorloadsread	save_jobsloggerwarningrc   IOError)fdatar   s      r   	load_jobsr   @  s   MMM 	)S7333 	(q9Q<<D88FB''	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	( 	(    
	iw777 1z!&&((5999xx++ _dOOONN#]^^^                      	 	 	IIIII	   			s~   B *A9,B 9A==B  A=B ED5-A+D'D5$E'D+	+D5.D+	/D55EEEEEr   c                    t                       t          j        t          t          j                  dd          \  }}	 t          j        |dd          5 }t          j	        | t                                                      d|d	           |                                 t          j        |                                           d
d
d
           n# 1 swxY w Y   t          j        |t                     t!          t                     d
S # t"          $ r( 	 t          j        |           n# t&          $ r Y nw xY w w xY w)zSave all jobs to storage..tmpz.jobs_dirsuffixprefixwr   r   )r   
updated_atr=   )indentN)r9   tempfilemkstempr   r   parentr(   fdopenr   dumprj   rh   flushfsyncfilenore   r0   BaseExceptionunlinkr*   )r   fdtmp_pathr   s       r   r   r   [  ss   MMM#I,<(=(=fU]^^^LBYr3111 	!QIt;==3J3J3L3LMMqYZ[[[[GGIIIHQXXZZ   	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	
8Y'''Y   	Ih 	 	 	D	sU   D A3CD CD C1D 
ED21E2
D?<E>D??Epromptnamerepeatdeliveroriginmodelproviderbase_urlc                 (   t          |          }||dk    rd}|d         dk    r|d}||rdnd}t          j                    j        dd         }t	                                                      }t          ||          }t          |t                    r!t          |          	                                nd}t          |	t                    r!t          |	          	                                nd}t          |
t                    r4t          |
          	                                
                    d	          nd}|pd}|pd}|pd}| p|r|d         ndp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ddddd|d t          |          ddd||d!}t                      }|                    |           t          |           |S )"a.  
    Create a new cron job.

    Args:
        prompt: The prompt to run (must be self-contained, or a task instruction when skill is set)
        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

    Returns:
        The created job dict
    Nr   rP   r[   r<   r   local   /zcron jobidr   2   r   r   r   r   r   r   rL   schedule_displayrR   r   )times	completedenabledTstate	scheduled	paused_atpaused_reason
created_atnext_run_at)rw   last_status
last_errorr   r   )rq   uuiduuid4hexrj   rh   r   r   r   r   rstripr$   r   r   r   r   )r   rL   r   r   r   r   r   r   r   r   r   parsed_schedulejob_idr   normalized_skillsnormalized_modelnormalized_providernormalized_base_urllabel_sourcer!   r   s                        r   
create_jobr   n  s   @ %X..O fkk v&((V^ $1(('Z\\crc"F
--
!
!
#
#C-eV<<-7s-C-CMs5zz'')))3=h3L3LV#h----///RV?I(TW?X?Xb#h----//66s;;;^b'/4-5-5S7HR033dbXbLf1SbS)//11 	& 	#	
 	):D"1%% 	! 	' 	' 	O 	O//	8DD 	
 
 	4  	!" 	T#$ 	%& 	c'( 	'88)* 5  C: ;;DKKdOOOJr    r   c                 f    t                      }|D ]}|d         | k    rt          |          c S  dS )zGet a job by ID.r   N)r   r%   )r   r   r!   s      r   get_jobr     sG    ;;D , ,t9&s+++++ 4r    include_disabledc                 R    d t                      D             }| sd |D             }|S )z2List all jobs, optionally including disabled ones.c                 ,    g | ]}t          |          S r8   r%   rU   js     r   
<listcomp>zlist_jobs.<locals>.<listcomp>  s!    888q""888r    c                 >    g | ]}|                     d d          |S )r   T)r$   r   s     r   r   zlist_jobs.<locals>.<listcomp>  s+    :::a155D#9#9::::r    )r   )r   r   s     r   	list_jobsr     s9    88IKK888D ;::4:::Kr    updatesc           
      0   t                      }t          |          D ]v\  }}|d         | k    rt          i ||          }d|v }d|v sd|v rJt          |                    d          |                    d                    }||d<   |r|d         nd|d<   |rs|d         }|                    d|                    d|                    d                              |d<   |                    d	          d
k    rt          |          |d<   |                    dd          rF|                    d	          d
k    r-|                    d          st          |d                   |d<   |||<   t          |           t          ||                   c S dS )zCUpdate a job by ID, refreshing derived schedule fields when needed.r   rL   r   r   r   Nr   rR   r   pausedr   r   T)r   	enumerater%   r   r$   r   r   )	r   r   r   ir!   updatedschedule_changedr   updated_schedules	            r   
update_jobr     s   ;;DD// , ,3t9%&8&8&899%0w'W"4"4 5gkk'6J6JGKKX`LaLa b b 1GH7HR033dGG 	L&z2*1++" $$Y<N0O0OPP+ +G&' {{7##x//)9:J)K)K&;;y$'' 	KGKK,@,@H,L,LU\U`U`anUoUo,L%5gj6I%J%JGM"Q$"47+++++4r    reasonc                 h    t          | ddt                                                      |d          S )z Pause a job without deleting it.Fr   )r   r   r   r   )r   rj   rh   )r   r   s     r   	pause_jobr     s=    $0022#		
 	
  r    c           	      ~    t          |           }|sdS t          |d                   }t          | dddd|d          S )z=Resume a paused job and compute the next future run from now.NrL   Tr   r   r   r   r   r   )r   r   r   )r   r!   r   s      r   
resume_jobr     sY    
&//C t"3z?33K !&	
 	
	 	 	r    c           	          t          |           }|sdS t          | ddddt                                                      d          S )z1Schedule a job to run on the next scheduler tick.NTr   r   )r   r   rj   rh   )r   r!   s     r   trigger_jobr     sX    
&//C t !&==2244	
 	
	 	 	r    c                      t                      }t          |          } fd|D             }t          |          |k     rt          |           dS dS )zRemove a job by ID.c                 ,    g | ]}|d          k    |S )r   r8   )rU   r   r   s     r   r   zremove_job.<locals>.<listcomp>3  s'    111!qw&00A000r    TF)r   r`   r   )r   r   original_lens   `  r   
remove_jobr   /  sV    ;;Dt99L1111t111D
4yy<$t5r    successerrorc                    t                      }t          |          D ]A\  }}|d         | k    r.t                                                      }||d<   |rdnd|d<   |s|nd|d<   |                    d          r|d                             d	d
          dz   |d         d	<   |d                             d          }|d         d	         }|3|d
k    r-||k    r'|                    |           t          |            dS t          |d         |          |d<   |d         d|d<   d	|d<   n|                    d          dk    rd|d<   t          |            dS Ct          |           dS )z
    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.
    r   rw   okr   r   Nr   r   r   r   r<   r   rL   r   Fr   r   r   r   )r   r   rj   rh   r$   popr   r   )	r   r   r   r   r   r!   r   r   r   s	            r   mark_job_runr  :  s    ;;DD//  3t9--))++C!$C)0!=gC-4 >$C wwx   
-0]->->{A-N-NQR-RHk* H))'22M+6	$yE7I7IHHQKKKdOOOFF "2#j/3!G!GC =!)!&I*G!!X--*GdOOOFF= @ dOOOOOr    c                 z   t                      }|D ]}|d         | k    r|                    di                               d          }|dvr dS t                                                      }t	          |d         |          }|r0||                    d          k    r||d<   t          |            dS  dS d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.
    r   rL   rP   r   rO   Fr   T)r   r$   rj   rh   r   r   )r   r   r!   rP   r   new_nexts         r   advance_next_runr  f  s     ;;D  t977:r**..v66D///uu--))++C'J==H H(>(>>>%-M"$tt55  5r    c            	      f   t                      } t                      }d t          j        |          D             }g }d}|D ]}|                    dd          s|                    d          }|st          |                    di           | |                    d                    }|sm||d<   |}t                              d	|                    d
|d                   |           |D ]}|d         |d         k    r	||d<   d} nt          t          j
        |                    }	|	| k    r|                    di           }
|
                    d          }t          |
          }|dv r| |	z
                                  |k    r~t          |
|                                           }|rZt                              d|                    d
|d                   |||           |D ]}|d         |d         k    r	||d<   d} n|                    |           |rt!          |           |S )aO  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),
    the job is fast-forwarded to the next future run instead of firing
    immediately.  This prevents a burst of missed jobs on gateway restart.
    c                 ,    g | ]}t          |          S r8   r   r   s     r   r   z get_due_jobs.<locals>.<listcomp>  s!    DDDq""DDDr    Fr   Tr   rL   rw   rv   z:Job '%s' had no next_run_at; recovering one-shot run at %sr   r   rP   r  zSJob '%s' missed its scheduled time (%s, grace=%ds). Fast-forwarding to next run: %s)rj   r   copydeepcopyr$   r|   r   inforu   r   rd   r   r   r   rh   r   r   )r   raw_jobsr   due
needs_saver!   r   recovered_nextrjnext_run_dtrL   rP   r   r  s                 r   get_due_jobsr    s    --C{{HDDDM(,C,CDDDD
CJ 9 9wwy$'' 	77=)) 	8
B''GGM22  N
 " !/C%HKKLD	**  
   d8s4y(((6B}%!%JE )
 $H$:8$D$DEE#wwz2..H<<''D
 +844E+++{1B0Q0Q0S0SV[0[0[ ,HcmmooFF KK:D	22     ' " "d8s4y0008B}-)-J!E 1 JJsOOO (Jr    c                    t                       t          | z  }|                    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          j        ||           t!          |           n5# t"          $ r( 	 t          j        |           n# t&          $ r Y nw xY w w xY w|S )zSave job output to file.Tr2   z%Y-%m-%d_%H-%M-%Sz.mdr   z.output_r   r   r   r   N)r9   r7   r6   r-   rj   ri   r   r   r   r(   r   writer   r   r   re   r0   r   r   r*   )r   r   job_output_dir	timestampoutput_filer   r   r   s           r   save_job_outputr    s   MMM&(N555&&':;;I i#4#4#44K#N(;(;FS]^^^LBYr3111 	!QGGFOOOGGIIIHQXXZZ   	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	
8[)))[!!!!   	Ih 	 	 	D	 sU   D3 'AD7D3 DD3 
D'D3 3
E%>EE%
E E%E  E%)NN)N)	NNNNNNNNN)F)>__doc__r	  r   loggingr   r(   rC   r   r   r   pathlibr   hermes_constantsr   typingr   r   r	   r
   	getLogger__name__r   hermes_timer   rj   r   rb   ImportError
HERMES_DIRr5   r   r7   rz   r   r   r%   r-   r0   r9   rF   rK   rq   ru   r|   r#   r   r   r   r   r   r   boolr   r   r   r   r   r   r  r  r  r  r8   r    r   <module>r$     s        				 				  ( ( ( ( ( ( ( (       , , , , , , , , , , , , , , , , , ,		8	$	$ * * * * * *!!!!!!LL   LLL _
{"	 
  # x} X\]`Xa    "T#s(^ S#X    d    t      %c %c % % % %*VS VT#s(^ V V V Vr$h $8 $ $ $ $. "&	  38n	 #	
 c]   6T c    @ tCH~ HSM U]^aUb    H4S#X'    6Dc3h(    ,  !'+"&""[ [[[ 3-[ SM	[
 c][ T#s(^$[ C=[ T#Y[ C=[ sm[ sm[ 
#s(^[ [ [ [|C HT#s(^4      d38n1E    s T#s(^ c3h8P    B
 
c 
8C= 
HT#s(^<T 
 
 
 
s xS#X7    & c3h 8    "s t    ) ) )t )HSM ) ) ) )XS T    8Ld4S>* L L L L^C       s   A A)(A)