0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

長時間ジョブを「サブステップ冪等化」で救う ― vault-auto-ingest が timeout 地獄を抜けた方法

0
Posted at

「Claude Code環境」シリーズの続きです。前回は Google Calendar を Vault の ground truth にした話 を書きました。今回は同じ vault-auto-ingest ジョブで起きた timeout 地獄と、それを「サブステップ冪等化」で抜け出した話です。

毎朝 hot.md が更新されない状態が3日続いたとき、原因は「launchd が発火していない」ではなく「発火したけど途中で死んでいた」でした。外側の再試行(複数スロット発火)は既に入れていた。問題は内側にあった。

困りごと:step2 が毎日 timeout して hot.md が3日凍結した

vault-auto-ingest.sh の step2 は、直近28時間分の会話ログを消化して Vault の wiki/ を更新するステップです。活動が多い時期、このステップに Claude Code ログ + Codex ログ が両方どっさり積み上がり、全記事リライトまで含めると40分の枠に収まらなくなりました。

# vault-auto-ingest.sh L26-28 のコメント(実際の記述)
# step2 は活動多発期に28h分のClaude+Codexログ消化+全記事リライトが40分枠に収まらず
# 連日timeout(hot.md凍結の真因, 2026-06-11〜13)。

plist は 4:55 / 8:20 / 10:45 / 12:15 の4スロットを持っています。これは外側の再試行で、「今日成功した」マーカー(DONE_MARKER)があれば後続スロットはスキップする設計です。でも DONE_MARKER が立つ前提は step2 が通ること ―― そのstep2が毎回 timeout するのでマーカーが永遠に立たず、4スロット全滅が続きました。

外側の再試行(複数スロット)は「今日どこかで成功させる」ための仕組みで、「途中まで成功した部分を生かす」機能はありません。それが内側の再開ロジックが必要な理由です。

設計:ソース別に分割し各サブステップに独立マーカーを持たせる

解決策は step2 を「Claude ログ消化」と「Codex ログ消化」の2本に割り、それぞれが独立したマーカーファイルを持つ形に変えることでした。

STEP2_MARKER="$HOME/.claude/logs/.vault-ingest-step2-done-${TODAY}"
STEP2A_MARKER="$HOME/.claude/logs/.vault-ingest-step2a-claude-${TODAY}"
STEP2B_MARKER="$HOME/.claude/logs/.vault-ingest-step2b-codex-${TODAY}"

マーカーは3層構造になっています。

マーカー 意味
STEP2A_MARKER Claude ログ消化が本日完了
STEP2B_MARKER Codex ログ消化が本日完了
STEP2_MARKER 両方完了(合成マーカー)

そしてこの3層を ingest_src() 関数で統一的に扱います。

ingest_src() {  # $1=マーカー $2=ソースdir $3=ソース名 $4=timeout秒 $5=追加指示
  local marker="$1" src="$2" name="$3" to="$4" extra="$5"
  [ -f "$marker" ] && { echo "[$(date '+%F %T')] step2($name) は本日実施済み — skip" >> "$LOG"; return 0; }
  cd "$VAULT" && run_to "$to" "$CLAUDE" -p \
"... ${src} に直近28時間で追加・更新されたファイル(${name}由来の会話ログ)だけを対象に ... ${extra}" \
    --dangerously-skip-permissions >> "$LOG" 2>&1 \
    && { touch "$marker"; echo "[$(date '+%F %T')] step2($name) 完了" >> "$LOG"; return 0; } \
    || { echo "[$(date '+%F %T')] WARN: step2($name) 失敗/timeout(次スロットで再試行)" >> "$LOG"; return 1; }
}

肝は2行目の [ -f "$marker" ] && return 0 です。マーカーがあれば即return、なければ claude -p を走らせてマーカーを立てる。これだけで「同日の再試行で完了済みサブを繰り返さない」冪等性が得られます。

呼び出しはこうなっています。

ingest_src "$STEP2A_MARKER" "$KB/raw/conversations/"       "claude" 1500 ""
ingest_src "$STEP2B_MARKER" "$KB/raw/codex-conversations/" "codex"  1500 "Codex由来でも既存記事に統合し、Claude側と重複する話題は新記事を作らず追記でまとめろ。"
# 両サブが済んだ時だけ合成マーカーを立てる
[ -f "$STEP2A_MARKER" ] && [ -f "$STEP2B_MARKER" ] && touch "$STEP2_MARKER"

timeout は各1500秒(25分)。元の「両方まとめて40分枠」を「各25分×2本・独立」に変えたことで、一方が timeout しても翌スロットでその一方だけ再試行できるようになりました。

どの粒度でマーカーを切るか

「全部を1マーカー」から「1ステップ1マーカー」まで粒度の選択肢があります。今回の基準はこうです。

マーカーを切る境界 = 「独立して再実行できる作業単位」

Claude ログと Codex ログは別ソースで、一方が終わっても他方は無関係に処理できます。これが独立単位の条件です。逆に「hot.md の最終整合」は両方の結果を見て1回やるべき作業なので、2本の ingest の後に独立して1回置く形が正しい。

分割しすぎると管理コストが上がります(マーカーファイルが乱立し、デバッグのとき追いにくい)。今回は「ソース種別」を粒度にしましたが、もし1ソースの消化に30分超かかる量になれば「ソース × 時間窓」に細分化する、というのが次の手です。

合成マーカーのロジック:AND 条件でデグレを防ぐ

[ -f "$STEP2A_MARKER" ] && [ -f "$STEP2B_MARKER" ] && touch "$STEP2_MARKER"

AND 条件で STEP2_MARKER を立てるのが重要で、OR にしてはいけません。OR にすると「Codex ログが timeout して消化できていない日でも step2 完了扱いになり、翌日の Codex ログ量がさらに膨れる」という悪循環に入ります。

STEP2_MARKER がないまま当日のスロットが4本全部終わると、DONE_MARKER も立ちません(DONE_MARKER を立てる直前に today-brief.md の鮮度チェックがあり、ingest が不完全だとブリーフが古いまま → 鮮度不足で DONE_MARKER を書かない設計になっています)。翌日の 4:55 スロットがゼロから走るのではなく、STEP2A/B のうち未完のものだけ走る ―― これが「内側の再開」の動作です。

multi-slot-launchd-retry(外側の再試行)と substep-idempotent-marker(内側の再開)の組み合わせが揃って初めて「今日どこかで必ず完了する」が保証されます。どちらか片方だけでは4スロット全滅が起こりえます。

踏んだ落とし穴

  • timeout 秒を短く切りすぎて常に失敗 → ソースの量に余裕を持たせた1500秒に。活動が少ない日は1分で終わるので、长めに設定して損はない
  • STEP2_MARKER が先に立っていると両サブの skip 判定より前に抜けるSTEP2_MARKER チェックを ingest_src 呼び出しの外(上位)に置き、入口で早期return させた
  • マーカーの日付を TODAY=$(date +%Y%m%d) で固定しているが、日付跨ぎランの場合に翌日のマーカーに書くTODAY はスクリプト冒頭の1回だけで決定するので実際には跨がない。ただし意識しておかないとはまる
  • Codex ログのソース (raw/codex-conversations/) が空の日は ingest_src が空振りで正常終了し STEP2B_MARKER が立つ → 空振り正常終了なのでマーカーが立つのは正しい。問題なし
  • スリープ凍結でマーカーが半端に残るケース → caffeinate -s(AC電源時のみ有効)+ 次スロットでの再開で対処済み。凍結したランが残したマーカーはその日の実績として正しく扱われる

まとめ

  • step2 が timeout する根本原因は「2ソースを1枠に詰め込んでいた」こと。分割が先
  • ingest_src() 関数で「マーカーがあればスキップ、なければ実行してマーカーを立てる」を1関数に閉じ込めた
  • マーカーの粒度は「独立して再実行できる作業単位」で切る。細かすぎると管理コストが上がる
  • 合成マーカーは AND 条件。OR にすると不完全消化が永続する悪循環に入る
  • 外側の再試行(複数スロット)と内側の再開(サブステップ冪等化)は役割が違う。両方要る

次回は、ingest の結果を受け取る側 ―― hot.md の内容が膨らみすぎて Claude のコンテキスト圧迫が始まった問題と、Obsidian の [[WikiLink]] 構造を使って参照を遅延解決する設計を書きます。


Lily@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています

皆さんの ❤️ やシェアが励みになります!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?