Lo probaste una vez y el recordatorio nunca llegó: las ejecuciones manuales de cron ahora son un disparo extra


Programas un recordatorio para dentro de 30 minutos —sacar la sartén del fuego— y quieres asegurarte de que funciona de verdad. Así que pulsas Run en el dashboard. Se dispara al instante, la salida tiene buena pinta, cierras la pestaña. Media hora después la cocina huele a metal quemado: el recordatorio nunca llegó.

El scheduler no estaba roto. Tu prueba se comió la única ocasión del one-shot. El job se dio por terminado y se retiró sin hacer ruido.

Antes, una ejecución manual contaba como la ejecución

Hay dos campos en un registro de cron que se confunden con facilidad: cuándo debe dispararse la próxima vez (next_run_at) y cuánto presupuesto está gastado (repeat.completed sobre repeat.times). Un one-shot no es más que un job con presupuesto de uno.

El código antiguo trataba un disparo manual como una ejecución normal: ejecutaba y luego escribía la contabilidad, incrementando repeat.completed. En un one-shot eso significaba 1 de 1, así que el registro quedaba marcado como completed, con enabled=false y next_run_at=null. Es un estado terminal. La ejecución programada de verdad no tuvo ninguna oportunidad. Los jobs recurrentes perdían ejecuciones igual: con repeat.times=3 te quedaban dos después de una sola prueba manual.

La nueva regla: una ejecución manual es un disparo extra

El comportamiento ahora es literal: un run-now no gasta la ocurrencia programada ni toca el presupuesto. Leídos de jobs.json, esos mismos dos jobs quedan así:

Job Antes (tras una ejecución manual) Ahora
Recordatorio one-shot in 30m completed, enabled=false, next_run_at=null, presupuesto 1/1: la ejecución programada nunca se dispara scheduled, enabled=true, next_run_at sigue en la hora original, presupuesto 0/1
every 60m con repeat.times=3 presupuesto anotado como 1/3, next_run_at desplazado a “ahora” presupuesto 0/3, next_run_at sin cambios

Una ejecución manual son horas extra: un turno adicional por encima, mientras el turno programado sigue presentándose y el presupuesto queda intacto.

El único caso en que sí es la ejecución programada

Si la ocasión del job ya venció cuando pulsas Run, esa ejecución es la ocurrencia programada: no te llevas dos ejecuciones. Las reglas, en la práctica:

  • disparas antes de que venza la ocasión → disparo extra, la ocasión sobrevive;
  • disparas cuando ya venció → es la ejecución programada, sin duplicado;
  • la ocasión vence mientras tu ejecución manual sigue en marcha → un one-shot se dispara inmediatamente después, mientras que un job recurrente descarta la ocasión pasada y toma su siguiente ocurrencia natural, igual que haría una ejecución programada.

Cómo usarlo

# dispara una vez ahora; se conserva la siguiente ejecución programada
hermes cron run <job_id>

# inspecciona los jobs: aquí viven next_run_at y los contadores de repeat
hermes cron list

# crea un one-shot
hermes cron create "in 30m" "Take the pan off the heat before you keep coding"

El botón Run del dashboard, un agente llamando a la herramienta cronjob_manage y una ejecución reenviada por el gateway pasan todos por la misma ruta, así que no hay división entre “seguro en la CLI, destructivo en la interfaz”.

Por qué es más difícil de lo que parece

La dificultad está en un crash entre decidir y anotar. El claim ocurre una sola vez, bajo el lock de jobs: cuando el disparo es un run-now explícito y la ocasión guardada todavía está en el futuro, el claim marca esa ocasión en el registro del disparo y devuelve next_run_at en la misma escritura. Si el proceso muere después del claim pero antes de completar, el disco sigue apuntando al instante futuro correcto, así que no se pierde ninguna ocurrencia.

La ruta reenviada por el gateway (trigger_job) sobrescribía next_run_at con “ahora” y no guardaba copia; ahora conserva la ocasión sobrescrita y la restaura en el momento del claim, de modo que los disparos repetidos no pueden desviar el calendario.

El cambio también tapó un agujero de visibilidad: un one-shot cuyo dispatch se había reclamado pero cuya ejecución nunca terminó se eliminaba sin salida, sin error y sin rastro. Ahora escribe un pequeño diagnóstico en el directorio de salida del job —dispatch reclamado, ejecución no completada, proceso probablemente matado— para que la desaparición al menos se pueda explicar.

Dos detalles que conviene saber

  • Una pausa global no bloquea las ejecuciones manuales. Mientras hermes pause está activo no arranca nada programado, pero un run-now explícito es una decisión del operador y se ejecuta igual. Es otra razón para que no gaste presupuesto: ponerse al día a mano durante una pausa no debería comerse la ejecución real posterior.
  • Una ejecución manual no es “saltar a la siguiente”. No adelanta next_run_at. Para saltarte una ocurrencia, usa pause / resume o edit sobre el calendario.

Para una revisión de toda la flota, usa cron doctor. Los calendarios entre zonas horarias están en la guía de zona horaria y DST, y la identidad por profile más el alcance de secretos en aislamiento de secretos en disparos enrutados. La lista de flags está en la página de referencia de hermes cron.

Estado del release

Integrado en main el 28 de octubre de 2026 (PR #146113). El release actual, v0.22.5 (23 de octubre de 2026), mantiene la semántica antigua, así que en una build anterior una prueba manual todavía puede retirar un one-shot en el acto.

Antes de fiarte de un recordatorio quieres la prueba de que va a sonar. Por fin esa premisa es cierta.