Hermes Agentのcronに再開地点を残す|notepadで二重処理を避ける

夜間の cron がfailedで止まりました。すぐに再実行すると、同じ投稿を 2 回作るおそれがあります。失敗したのが投稿前とは限らないからです。外部 API では作成に成功し、その後の保存だけが失敗した可能性もあります。
私はfailedを再実行の合図にしません。まず実行履歴と外部側の結果を照合します。そのうえで、Hermes Agent の cron notepad へ処理済み ID を残します。
notepad は小さな再開地点に向いています。ただし、これだけですべての二重処理は防げません。外部 API への書き込みと notepad の更新は、1 つの取引として確定しないためです。
前回完了時から今回完了後まで
前回の「Hermes Agentのcronをモデル変更から守る」では、無人 job が意図しないモデルへ移るのを防ぎました。provider と model を固定し、drift guard の状態を確認しています。
今回は、job が起動した後の再開判断を扱います。
| 時点 | 状態 |
|---|---|
| 前回完了時 | cronが使うproviderとmodelを固定できました |
| 今回開始時 | 失敗後に、どこまで処理できたか判断できません |
| 今回完了後 | runs、外部結果、notepadを照合してから再実行できます |
モデル固定が済んでいない場合は、前回の手順を先に実施します。今回の操作は、推論先の変更を修復するものではありません。
なぜ再実行より照合が先なのか
Hermes Agent は、cron の実行を attempt として台帳へ記録します。状態はclaimed、running、completed、failed、unknownです。
failedは、外部サービスに何も作られなかったことを意味しません。たとえば、次の順番で動く job を考えます。
- 記事ファイルを作ります
- Git の branch を push します
- GitHub で PR を作ります
- 完了結果を返します
3 番まで成功し、4 番で通信が切れれば、attempt は失敗として見える可能性があります。ここで job 全体を再実行すると、別 branch や重複 PR を作りかねません。
unknownにも注意が必要です。公式仕様では、再起動後に所有 process が消えたと確認された attempt がunknownになります。Hermes Agent は、この attempt を自動再実行しません。
この動作は安全側です。処理済みか分からない仕事を、推測で繰り返さないためです。
用語表
| 用語 | この回での意味 |
|---|---|
| job | schedule、prompt、delivery先などを持つcronの定義です |
| attempt | jobが1回起動した記録です |
| execution ledger | attemptの状態を保存する実行台帳です |
| notepad | jobごとにkeyとvalueを保存する小さな永続領域です |
| 再開地点 | どの対象まで処理したかを示す値です |
| 外部副作用 | 投稿、PR作成、メール送信、課金APIなど、実行先に変更を残す操作です |
| 冪等性 | 同じ依頼を繰り返しても、結果が重複しない性質です |
再開判断の流れ
今回の境界を図にすると、次の形です。
Hermes Agent cron
│
├── execution ledger
│ └── attemptの状態を確認
│
├── output directory
│ └── 前回の出力を確認
│
└── job notepad
└── 最後に確認できた対象IDを保存
外部サービス
└── 投稿ID、PR URL、commitなどを照合
照合結果
├── 処理済み 再実行しない
├── 未処理 安全確認後に再実行
└── 判断できない 停止して人が確認
.tick.lockは、scheduler tick 同士の重複を防ぎます。ただし、外部 API に同じ投稿を 2 回送る意味上の重複までは保証しません。scheduler の排他と、業務データの重複防止は分けて考えます。
この章を飛ばせる人
次の条件をすべて満たす job では、notepad の導入を飛ばせます。
- 読み取り専用で、外部へ変更を残しません
- 同じ入力で何度動いても結果が増えません
- 出力先が内容 hash などで一意になります
- 外部サービス側で idempotency key を使っています
- 再実行前に処理済み状態を API から取得できます
1 つでも満たさない場合は、再開地点を持つ価値があります。ただし、決済や公開処理では notepad だけに頼りません。外部サービス側の一意制約や idempotency key を優先します。
検証用jobを選ぶ
本番の投稿 job や公開 job では試しません。すでに登録済みの、読み取り専用の検証 job を使います。
まず job 一覧から ID を確認します。
hermes cron list
表示された検証用 job の ID を、環境変数へ入れます。
JOB_ID='<your-test-job-id>'
printf '%s\n' "$JOB_ID"
<your-test-job-id>は説明用の記号です。実際の出力ではありません。自分の環境にある検証用 job ID へ置き換えます。
空文字のまま進めないように確認します。
test -n "$JOB_ID"
成功時は何も表示されず、終了コードは0です。
直近のattemptを確認する
次に、対象 job の実行履歴を 5 件に絞ります。
hermes cron runs "$JOB_ID" --limit 5
表示内容は job ごとに変わります。確認する項目は、attempt の時刻と状態です。公式仕様で使われる状態は次の 5 つです。
claimed
running
completed
failed
unknown
これは固定の実行例ではなく、状態名の一覧です。実際のコマンドは、その job に保存された行を表示します。
completedでも外部結果を確認します。agent の処理完了と、通知先への delivery 成功は同じ判定ではありません。
failedまたはunknownなら、まだhermes cron runを実行しません。次の順で確認します。
- cron の出力ファイルを確認します
- Git の branch、commit、PR を確認します
- 外部サービスの投稿 ID や作成時刻を確認します
- job が保存した再開地点を確認します
cron の出力は、公式仕様では次の場所に保存されます。
~/.hermes/cron/output/{job_id}/{timestamp}.md
{job_id}と{timestamp}は説明用のプレースホルダです。パスを組み立てて削除せず、読み取りだけで確認します。
notepadの現在値を読む
job の notepad を一覧表示します。
hermes cron notepad "$JOB_ID" list
初回は key が存在しない場合があります。表示形式は Hermes Agent の版で変わる可能性があります。固定した文言を期待せず、終了コードと key の有無を確認します。
終了コードは直後に取得します。
printf 'exit=%s\n' "$?"
正常終了なら、期待する形は次のとおりです。
exit=0
検証用の再開地点を保存する
検証では、demo-001を処理済み ID の代わりに使います。実在する投稿 ID や秘密は保存しません。
hermes cron notepad "$JOB_ID" set last_item demo-001
setの表示文言には依存しません。続けてgetで読み返します。
hermes cron notepad "$JOB_ID" get last_item
読み返した値として確認する内容です。
demo-001
さらに一覧からも確認します。
hermes cron notepad "$JOB_ID" list
last_itemとdemo-001の組が確認できれば、再開地点を保存できています。
外部副作用の前後を分ける
実際の job では、次の順番を守ります。
1. 対象IDを決めます
2. notepadの処理済みIDを読みます
3. 外部サービス側にも同じIDがないか照合します
4. 未処理なら外部操作を1回だけ実行します
5. 外部側の作成済みIDを読み返します
6. 成功を確認してからnotepadを更新します
7. 最終結果を返します
大切なのは 5 番です。HTTP 応答を受けただけで成功扱いにせず、作成先から読み返します。
それでも隙間は残ります。外部操作が成功した直後、notepad 更新前に process が落ちる場合です。次回は notepad だけを見ると未処理に見えます。
そのため、外部サービス側の照合を省けません。API が idempotency key を提供するなら、対象 ID から安定した key を作ります。データベースなら、一意制約を使います。GitHub PR なら、head branch や open PR を検索してから作成します。
notepad は判断材料です。単独で exactly-once を保証する仕組みではありません。
failedとunknownから安全に再開する
再実行の前に、3 つの結果を並べます。
| 確認先 | 見るもの | 判断 |
|---|---|---|
| execution ledger | failedまたはunknownになった時刻 | 失敗したattemptを特定します |
| 外部サービス | 対象ID、作成時刻、URL | すでに副作用が発生したか確認します |
| notepad | last_itemなどの再開地点 | jobが最後に確認できた対象を確認します |
外部側に対象が存在すれば、job 全体を再実行しません。必要なら、未完了の後半だけを別の安全な手順で進めます。
外部側と notepad の両方に対象がなく、未処理だと確認できた場合だけ再実行を検討します。
hermes cron run "$JOB_ID"
このコマンドは、その場で job の完了を返すものではありません。公式仕様では、次の scheduler tick での実行を trigger します。gateway scheduler は通常 60 秒ごとに tick します。
実行を予約した後は、履歴を再確認します。
hermes cron runs "$JOB_ID" --limit 5
新しい attempt がcompletedになるまで、成功とは報告しません。外部副作用を伴う job では、外部側の結果も再度読み返します。
doctorとincidentsを補助に使う
job の定義や scheduler 全体に問題がないか、doctor で確認します。
hermes cron doctor
個別 attempt の代わりではありません。設定不備の候補を探す補助診断です。
incident の一覧も確認できます。
hermes cron incidents list
状態で絞る場合は、detected、alerted、closedを使います。
hermes cron incidents list --state detected
incident を ack する前に、原因と外部結果を確認します。ack は処理の巻き戻しではありません。既知の incident として扱う操作です。
検証用keyを削除する
検証が終わったら、作成した key だけを削除します。job や他の key は消しません。
hermes cron notepad "$JOB_ID" delete last_item
削除後に読み返します。
hermes cron notepad "$JOB_ID" list
last_itemがないことを確認します。戻り文言ではなく、削除対象の key が一覧から消えた事実を見ます。
よくあるエラー
| 症状 | 原因 | 対応 |
|---|---|---|
JOB_IDのjobが見つかりません | IDの転記ミスか、別profileを見ています | hermes cron listを同じprofileで実行します |
| notepadのkeyが空です | 初回実行か、key名が一致していません | listで全keyを確認し、綴りを揃えます |
failedなので再実行したら重複しました | 外部側の成功を照合していません | 投稿ID、branch、PR、commitを先に検索します |
unknownが残ります | 再起動時に元processの完了を確定できませんでした | 自動再実行せず、出力と外部結果を照合します |
run後すぐ履歴が増えません | 実行は次のscheduler tickで始まります | gatewayとcronの状態を確認し、次のtickを待ちます |
| 同じkeyを複数用途で上書きしました | notepadのkey設計が粗すぎます | last_prやlast_postのように用途を分けます |
| notepad更新前にprocessが落ちました | 外部操作とnotepad更新が同一transactionではありません | 外部サービスを検索し、idempotency keyや一意制約を併用します |
| doctorが正常でもjobが失敗します | doctorは外部APIの業務結果まで保証しません | runs、出力、外部側の3点を確認します |
運用へ入れる前の確認
再開地点を置くだけでは不十分です。job の完了条件にも、読み返しを含めます。
- 対象 ID は入力から安定して作ります
- 外部操作の前に、同じ ID がないか検索します
- 外部操作の後に、作成結果を読み返します
- 読み返し成功後に notepad を更新します
failedとunknownを自動再実行しません- 再実行前に branch、PR、投稿 ID、出力を確認します
- 判断できない場合は止めます
cron を止めないことより、重複を増やさないことを優先します。再開地点は速度のためではなく、停止後の判断を具体化するために置きます。
出典
| 資料 | 確認した内容 |
|---|---|
| Hermes Agent Scheduled Tasks | cronの実行台帳、attempt状態、unknownの扱い、tick lock、出力先、runの動作を確認しました |
| Hermes Agent CLI Commands Reference | hermes cronのCLI体系を確認しました |
| NousResearch/hermes-agent | Hermes Agentの公式ソースと更新履歴の参照先です |
CLI の usage は、2026 年 9 月 7 日にhermes cron --help、hermes cron notepad --help、hermes cron runs --help、hermes cron incidents --help、hermes cron doctor --helpで確認しました。版が変わった場合は、手元の help を正として引数を見直します。
