この記事が良かったら
「チップをリクエスト」で著者にチップの受け取り設定をお願いできます

― 毎晩自動で蒸留するパイプライン
この記事が良かったら
「チップをリクエスト」で著者にチップの受け取り設定をお願いできます
「Claude Code環境」シリーズです。記憶を4層に分けた話で「会話ログ=層1」「Obsidian Vault=層4」と書きましたが、今回はその層1から層4へ毎晩自動で蒸留する配管の実装を書きます。
会話ログは生のままだと巨大で読めません。一方Obsidianの人間Wikiは構造化された長期記憶です。この2つを「毎晩、直近28時間分だけ、ドメイン構造を守って追記する」のを無人で回しています。やってみて分かったサイレント失敗の潰し方と捏造させない仕掛けが、この記事の中身です。
launchdから毎朝起動して、おおまかに3工程です。
蒸留はソース別に2本(Claude会話とCodex会話)に分けています。理由は後述しますが、1本にまとめると時間枠に収まらず毎晩timeoutしていたからです。
最初に全部詰まったのがこれです。VaultはiCloud同期の ~/Documents 配下にあり、macOSのTCC(プライバシー保護)で守られています。launchdから git を走らせても、保護領域に書けずに失敗する。
しかも厄介なのは、これが「サイレント失敗」になりやすいこと。だからプリフライトで早期検知して大声でログ+通知します。
# launchd配下では ~/Documents(保護領域) に触れない場合がある。
# ここで早期検知し、サイレント失敗(exit 0偽装)を防ぐ。
if ! ( cd "$VAULT" && git rev-parse --git-dir >/dev/null 2>&1 ); then
echo "❌ FDA未付与: launchdから '$VAULT' にアクセス不可(TCC保護)" >> "$LOG"
notify_fail "FDA未付与(設定→フルディスクアクセス→/bin/bash を許可)"
exit 1
fi解決は「システム設定 → プライバシーとセキュリティ → フルディスクアクセス で /bin/bash を許可」。スクリプト自身は保護外(~/.claude/scripts/)に置きます。~/Documents に置くとlaunchdからexecできません。
symlinkで ~/ に逃がす案も試しましたが、iCloudがsymlinkを競合処理して壊すため不可でした。保護領域とiCloud同期が重なると、直感に反する制約が増えます。スクリプトは保護外・データは保護内、と置き場所を割り切るのが安定します。
蓋を閉じてスリープすると、夜間ジョブは凍結します。caffeinate -s はAC電源時しか効かないので、バッテリー駆動だと普通に止まる。そこで**「成功するまで複数スロットで再試行、成功したら即終了」**の自己回復型にしました。
DONE_MARKER="$HOME/.claude/logs/.vault-ingest-done-${TODAY}"
[ -f "$DONE_MARKER" ] && exit 0 # 本日成功済みなら即終了蒸留を2本(Claude/Codex)に割ったのも同じ理由です。活動の多い日は28時間分のログ消化+記事リライトが40分枠に収まらず連日timeoutし、それが原因でホットキャッシュが凍結していました。サブステップごとに独立マーカーを持たせ、片方がtimeoutしても次スロットが残りだけ再試行します。
これがいちばん大事な学びです。claude -p に「会話ログから今日のブリーフを書け」と投げると、ノートに無い予定の拘束時間や所要日数を、それらしく創作することがありました。「M/D〜M/D」という散文を見て「この期間ずっと拘束される」と膨らませてしまう。
対策は2つです。
① カレンダーの実体スナップショットを正典にする。 散文ではなく、Google Calendar APIから取った時刻付きの予定を ground truth として別ファイルに落とし、「これを唯一の正典スケジュールとして読め」と指示します。
② 取得失敗時はlast-known-goodを温存する。 APIが失敗しても、正典を空で潰さない。前回成功時の値を stale 印付きで残します。
if [ -n "$CAL_SRC" ]; then
cp "$CAL_TMP" "$CAL_SNAPSHOT"; cp "$CAL_SNAPSHOT" "$CAL_LASTGOOD"
elif [ -s "$CAL_LASTGOOD" ]; then
# 取得失敗: 前回goodを温存し ⚠️stale 印で再構築(last-known-good)
{ echo "⚠️ 本日取得失敗。以下は前回成功時点の値(stale)。"; tail -n +4 "$CAL_LASTGOOD"; } > "$CAL_SNAPSHOT"
fiそしてプロンプト側でも強く縛ります。「ノートに無い情報を推測で足すな」「日付・時間の主張は確定情報と推測を明示的に分けろ」。AIに長期記憶を書かせるなら、創作の余地を構造で潰すしかありません。
ブリーフを日付つきでアーカイブするとき、「このランの開始時刻より新しいファイルだけ」を今日の成果と認めます。
START_STAMP=$(mktemp ...) # ラン開始時刻スタンプ
# ...生成...
if [ -s "$BRIEF_SRC" ] && [ "$BRIEF_SRC" -nt "$START_STAMP" ]; then
cp "$BRIEF_SRC" "$ARCH_FILE"; touch "$DONE_MARKER" # 新しい時だけ完了扱い
else
notify_fail "ブリーフ未完 — 次スロットで自動再試行" # 古いまま=失敗、再試行へ
fiこれが無いと、生成がtimeoutしても「前日の古いブリーフ」をコピーして「成功」と記録してしまう。鮮度をmtimeで判定することで、失敗を成功に偽装しないようにしています。
次回は、この無人ジョブの親玉 ―― Claude Codeの自律ループを24時間回し、コストで暴走を止めるを書きます。
Lily(@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています
◼︎作ったアプリは **ポートフォリオ** にまとめています📱
◼︎新着・開発の裏側は X **@bokuwalily** で発信しています🐦
◼︎OSS: **github.com/bokuwalily**🐙
皆さんの ❤️ やシェアが励みになります!