実践ガイド(2026.10.16 更新)

定期タスクの二重実行を止めるガードを8ケースで検証 ― state・lock・UTF-8

定期タスクが「もう実行済み」なのか、「途中で止まり、外部処理だけ済んだかもしれない」のかは同じではありません。前者は安全にskipできますが、後者を自動で再実行すると二重書き込みになる可能性があります。

そこで、日付・state・lockを使う標準Pythonの小さなガードを作り、初回、同日重複、翌日、壊れたstate、模擬中断後のレビュー停止、既存lock、cp932失敗、UTF-8成功の8ケースを固定入力で実行しました。8件とも期待どおりになりました。ただし、これはClaude Desktop自体の実機試験でも、exactly-once(厳密に1回)の証明でもありません。

完全なスクリプト、テスト、固定fixture、結果JSON、生ログ、manifestは検証教材ZIPから取得できます(14,467 bytes、SHA-256 8e8bd63bee00cacffc35408c50ebef73c7b5164e04513b9124fa35b38745888c)。実データや外部APIへは接続しません。

先に分ける:Claudeの公式仕様と、今回のPython実測

2026年10月16日にClaude Code Desktopの公式文書を再取得しました。現行の案内では、CodeタブのRoutinesからLocalの定期タスクを作ります。同じRoutines画面で作れるCloudのroutineはクラウドで動き、PCがオフでも実行できます。一方、DesktopのLocalタスクは手元のファイルへアクセスできますが、アプリが開きPCが起きている間だけ起動します。この記事は旧画面名を前提にしません。

公式文書で確認したLocalタスクの要点は次のとおりです。

  • 期限になると、開いている手動セッションとは別のfresh sessionを起動する。
  • アプリ起動時またはPC復帰時、直近7日以内に未実行があれば、最も新しい1回だけcatch-upし、それ以前は破棄する。
  • Manual permission modeで未許可ツールが必要になると、承認まで停止し、セッションはサイドバーに残る。
  • 作成後にRun nowで試し、必要なツールをalways allowにする案内がある。ただしMCPツールのrequiresUserInteractionは毎回確認され、always-allowを選べない。

以上は公式説明です。今回、Claude Desktopでタスクを作成・起動・停止させる操作はしていません。実測したのは、その前提で外部コマンド側に置けるPythonガードだけです。Claudeの料金、契約条件、Cloud routineの実行品質も検証対象外です。

検証条件

項目 条件
実行日 2026年10月16日
OS / shell Windows / PowerShell
Python 3.14.2、標準ライブラリのみ
入力日 初日2026-10-16、翌日2026-10-17に固定
外部処理 ローカルのeffect.jsonlへ1行追記するstub
state保存 同じdirectoryのtempへUTF-8 JSONを書き、flush・fsync・close後にos.replace
lock os.O_CREATとos.O_EXCLで新規作成。既存lockは削除・上書きしない
費用 既存のローカルPython環境だけを使い、新規支出なし

stateは外部stubの前にrunning、stub成功の後にcompletedとして保存します。壊れたstate、running、既存lockを検出した場合はeffectを変更しません。壊れたstateはbytesとSHA-256、既存lockはbytesを前後で照合しました。

実行手順

ZIPを展開し、展開先のscheduled_task_guardで次を実行します。PowerShellのコマンドです。

python -m unittest discover -s tests -p "test_*.py" -v
python run_cases.py --output-dir reproduced

2行目が次を返せば、reproduced/case_results.jsonとreproduced/raw.logに再現結果が残ります。

{"case_count": 8, "status": "pass"}

ガードの中心は次の順序です。以下は抜粋で、このブロック単体では動きません。完全版はZIPのscheduled_task_guard.pyにあります。

# 抜粋:エラー処理、引数定義、lock解放などは省略
acquire_lock(lock_path, run_date)  # O_CREAT | O_EXCL
state = load_state(state_path)

if state and state["status"] == "running":
    return EXIT_REVIEW_REQUIRED
if state and state["status"] == "completed" and state["run_date"] == run_date:
    return EXIT_DUPLICATE

atomic_write_json(state_path, {**identity, "status": "running"})
append_effect(effect_path, run_date, execution_id)
atomic_write_json(state_path, {**identity, "status": "completed"})

一時ファイルからstateを置換する箇所は、Python公式のos.replaceを使います。置換先と同じdirectoryにtempを作り、書き込み中のJSONを次の起動がそのまま読む範囲を狭めます。これだけで電源断時の永続性まで保証されるわけではありません。

8ケースの結果

定期タスクガード8ケースの終了コード、状態、effect差分

表のeffect Δは、外部処理の代わりに置いたローカルstubへの追記行数です。pre/post-stateは見やすさのため要約し、完全値はZIPの結果JSONに残しています。

case pre-state lock command exit status effect Δ post-state
first_run absent absent run 10/16 0 completed +1 completed 10/16
same_day_duplicate completed 10/16 absent run 10/16 10 duplicate_skip 0 不変
next_day_run completed 10/16 absent run 10/17 0 completed +1 completed 10/17
corrupt_state 不正UTF-8 JSON absent run 10/16 20 invalid_state 0 bytes/hash不変
crash_then_review_stop absent absent crash注入→run 10/16 30→22 review_required 0 running 10/16
existing_lock completed 10/15 present run 10/16 21 lock_exists 0 state/lock不変
cp932_failure n/a n/a PYTHONIOENCODING=cp932 ... emit 1 UnicodeEncodeError 0 n/a
utf8_success n/a n/a PYTHONIOENCODING=utf-8 ... emit 0 completed 0 n/a

同日2回目のduplicate_skipと、中断後のreview_requiredを別のstatusとexitにしたのが重要です。後者は「前回が失敗した」とも「成功した」とも断定せず、人がeffectの実行IDを照合するまで止めます。既存lockも勝手に期限切れとみなして消しません。

文字コードの2ケースでは同じJalapeño チップを出力しました。PYTHONIOENCODING=cp932を明示した実行はñを表現できず終了1、utf-8を明示した実行は終了0でした。Python公式どおり、これは標準入出力のencodingを上書きする環境変数です。Windows全体やファイル本文のencodingまで直す設定ではありません。

実務でどこまで防げて、どこから人の確認が要るか

このガードで区別できたのは、固定入力に対する「完了済み」「state不正」「成否不明のrunning」「既存lock」です。同日完了済みならskipし、曖昧なら自動再実行しない、という停止境界を作れます。

一方、厳密な二重実行防止やexactly-onceは主張できません。effect追記の直後、completed保存の直前に落ちれば、外部処理は済んでいるのにstateはrunningのままです。逆に、古いlockを人が誤って消せば並行実行を許すことがあります。本番では外部サービスにも一意な冪等キーを渡し、結果を照合してから再開・完了扱い・取消を決める必要があります。

未検証・非保証の範囲は次のとおりです。

  • NFSや別端末を含む分散filesystem、複数ホストの同時起動
  • OSやfilesystemを横断したos.replaceの耐障害性、directory sync、電源断、disk failure
  • effect成功後からcompleted保存前までのcrash window
  • 外部サービスの冪等性、network timeout、retry、部分成功
  • PID生存確認、lockの安全な期限切れ判定、強制終了そのもの
  • Claude DesktopのLocal/Cloud taskの実行成功率、権限画面、catch-upの実機再現

無人実行前の確認リスト

  1. 作成直後に手動実行し、通常完了と同日skipを別々に確認する。
  2. stateを壊した場合に初期化せず停止し、元bytesを保てるか確認する。
  3. runningや既存lockを自動削除せず、effectの一意IDを照合する手順を決める。
  4. 非ASCIIを含む実データで、標準入出力とファイルencodingを別々に確認する。
  5. 外部サービス側の冪等キー、結果取得、retry条件を確認する。
  6. Claude Desktopを使う場合はRun nowで権限を点検し、requiresUserInteractionのツールを無人前提にしない。

操作内容そのものを実行前に権限・期限・重複で止めたい場合は、AIエージェントの実行前ゲートを6ケースで検証した記事が関連します。公開前のbuildと内部リンクを点検したい場合は、Astro + Cloudflare Pagesの公開前確認を参照してください。

出典と更新履歴

  • Claude Code Desktop公式文書:2026年10月16日再取得。fresh session、missed runs、Manual permission、RoutinesのLocal/Cloud差を確認。Claude実機操作は未実施。
  • Python公式os.replace:2026年10月16日確認。教材は同一directoryのtempからstateを置換。
  • Python公式PYTHONIOENCODING:2026年10月16日確認。教材ではcp932とutf-8を明示比較。

2026年10月16日更新:従来の抜粋コードと2出力だけの記録を、完全実行可能な標準Python教材へ置き換えました。8ケース、5件のunittest、結果JSON、生ログ、manifest、ZIP、結果図を追加し、現行Routines UIとCloudとの差を反映しました。Claudeの公式仕様と自作ガードの実測を分離し、exactly-onceを保証しない限界を明記しました。

一次情報・出典

  1. Claude Code公式:Schedule recurring tasks in Claude Code Desktop
  2. Python公式:os.replace
  3. Python公式:PYTHONIOENCODING