Você disparou um teste e o lembrete nunca chegou: execuções manuais do cron agora são um disparo extra


Você cria um lembrete para daqui a 30 minutos — tirar a panela do fogo — e quer ter certeza de que a coisa funciona de verdade. Então clica em Run no dashboard. Dispara na hora, a saída parece certa, você fecha a aba. Meia hora depois a cozinha cheira a metal queimado: o lembrete nunca chegou.

O scheduler não estava quebrado. O seu teste consumiu a única vaga do one-shot. O job se considerou concluído e se aposentou em silêncio.

Uma execução manual costumava contar como a execução

Dois campos de um registro de cron são fáceis de confundir: quando ele deve disparar em seguida (next_run_at) e quanto do orçamento já foi gasto (repeat.completed de repeat.times). Um one-shot é simplesmente um job com orçamento de um.

O código antigo tratava um disparo manual como execução normal: rodava e depois gravava a contabilidade — repeat.completed incrementado. Para um one-shot isso significava 1 de 1, então o registro era marcado completed, enabled=false, next_run_at=null. É um estado terminal. A execução agendada de verdade nunca teve chance. Jobs recorrentes perdiam execuções do mesmo jeito: repeat.times=3 te deixava com duas restantes depois de um único teste manual.

A nova regra: uma execução manual é um disparo extra

O comportamento agora é literal — um run-now não consome a ocorrência agendada e não mexe no orçamento. Relidos a partir do jobs.json, os mesmos dois jobs ficam assim:

Job Antes (depois de uma execução manual) Agora
lembrete one-shot in 30m completed, enabled=false, next_run_at=null, orçamento 1/1 — a execução agendada nunca dispara scheduled, enabled=true, next_run_at ainda no horário original, orçamento 0/1
every 60m, repeat.times=3 orçamento registrado como 1/3, next_run_at empurrado para “agora” orçamento 0/3, next_run_at inalterado

Uma execução manual é hora extra: um turno a mais por cima, enquanto o turno agendado ainda aparece e o orçamento fica intocado.

O único caso em que ela é a execução agendada

Se a vaga do job já está vencida quando você clica em Run, essa execução é a ocorrência agendada — você não ganha duas execuções. As regras na prática:

  • disparar antes de a vaga vencer → disparo extra, a vaga sobrevive;
  • disparar depois de vencida → é a execução agendada, sem duplicata;
  • a vaga vence enquanto a sua execução manual ainda roda → um one-shot dispara imediatamente depois, enquanto um job recorrente descarta a vaga vencida e assume a próxima ocorrência natural, exatamente como faria uma execução agendada.

Como usar

# dispara uma vez agora; a próxima execução agendada é preservada
hermes cron run <job_id>

# inspecione os jobs: next_run_at e os contadores de repeat ficam aqui
hermes cron list

# cria um one-shot
hermes cron create "in 30m" "Tire a panela do fogo antes de continuar codando"

O botão Run do dashboard, um agent chamando a tool de execução do cronjob_manage e uma execução encaminhada pelo gateway passam todos pelo mesmo caminho, então não existe divisão entre “seguro na CLI, destrutivo na UI”.

Por que isso é mais difícil do que parece

A dificuldade é um crash entre decidir e registrar. O claim acontece uma vez, sob o lock de jobs: quando o disparo é um run-now explícito enquanto a vaga guardada ainda está no futuro, o claim estampa essa vaga no registro do disparo e recoloca next_run_at na mesma escrita. Se o processo morre depois do claim mas antes da conclusão, o disco ainda aponta para o instante futuro correto, então nenhuma ocorrência se perde.

O caminho encaminhado pelo gateway (trigger_job) costumava sobrescrever next_run_at com “agora” e não guardava cópia; agora ele salva a vaga sobrescrita e a restaura no momento do claim, então disparos repetidos não conseguem desviar o cronograma.

A mudança também fechou uma lacuna de visibilidade: um one-shot cujo dispatch foi reivindicado mas cuja execução nunca terminou era removido sem saída, sem erro e sem registro. Agora ele grava um pequeno diagnóstico no diretório de output do job — dispatch reivindicado, execução nunca concluída, processo provavelmente morto — então o desaparecimento ao menos é explicável.

Dois detalhes que vale saber

  • Uma pausa global não bloqueia execuções manuais. Enquanto hermes pause está ativo nada agendado começa, mas um run now explícito é uma decisão do operador e ainda executa. É mais um motivo para ele não gastar o orçamento: recuperar o atraso à mão durante uma pausa não deve comer a execução real depois.
  • Uma execução manual não é “pular para a próxima”. Ela não avança o next_run_at. Para pular uma ocorrência, use pause / resume, ou edit no cronograma.

Para uma revisão de frota inteira, use o cron doctor. Cronogramas entre fusos horários estão em o artigo sobre timezone e DST, e identidade por profile mais escopo de secret em isolamento de secrets em disparos roteados. A lista de flags fica na página de referência do hermes cron.

Status da release

Entrou na main em 28 de outubro de 2026 (PR #146113). A release atual, v0.22.5 (23 de outubro de 2026), ainda tem a semântica antiga, então em um build mais velho um disparo de teste ainda pode aposentar um one-shot na hora.

Antes de confiar em um lembrete, você quer prova de que ele vai tocar. Essa premissa finalmente é verdadeira.