Tsukutta

🧠会話ログをObsidianの長期記憶に変える

― 毎晩自動で蒸留するパイプライン

Lily2026年6月20日公開
約10分で読めます4

「Claude Code環境」シリーズです。記憶を4層に分けた話で「会話ログ=層1」「Obsidian Vault=層4」と書きましたが、今回はその層1から層4へ毎晩自動で蒸留する配管の実装を書きます。

会話ログは生のままだと巨大で読めません。一方Obsidianの人間Wikiは構造化された長期記憶です。この2つを「毎晩、直近28時間分だけ、ドメイン構造を守って追記する」のを無人で回しています。やってみて分かったサイレント失敗の潰し方捏造させない仕掛けが、この記事の中身です。

全体像:採取 → 蒸留 → バックアップ

launchdから毎朝起動して、おおまかに3工程です。

  1. 採取:会話ログ(層1)を最新化する(extract_conversations.py)
  1. 蒸留:claude -p ヘッドレスで、直近28時間の差分だけをVaultの該当ドメインに追記
  1. バックアップ:毎回 git commit + private repo に push(荒れてもrevert可能)

蒸留はソース別に2本(Claude会話とCodex会話)に分けています。理由は後述しますが、1本にまとめると時間枠に収まらず毎晩timeoutしていたからです。

罠1:launchdからは保護フォルダに触れない(TCC)

最初に全部詰まったのがこれです。VaultはiCloud同期の ~/Documents 配下にあり、macOSのTCC(プライバシー保護)で守られています。launchdから git を走らせても、保護領域に書けずに失敗する

しかも厄介なのは、これが「サイレント失敗」になりやすいこと。だからプリフライトで早期検知して大声でログ+通知します。

text
# 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同期が重なると、直感に反する制約が増えます。スクリプトは保護外・データは保護内、と置き場所を割り切るのが安定します。

罠2:スリープと二重実行 → 自己回復型にする

蓋を閉じてスリープすると、夜間ジョブは凍結します。caffeinate -s はAC電源時しか効かないので、バッテリー駆動だと普通に止まる。そこで**「成功するまで複数スロットで再試行、成功したら即終了」**の自己回復型にしました。

  • plistは 4:55 / 8:15 / 10:15 / 12:15 など複数スロットで発火
  • 本日分が成功済みなら、後続スロットは done マーカーを見て即 exit 0(空振りでログも汚さない)
  • 二重実行は mkdir ロックで排除(プロセス死活でstaleを自動回収)
text
DONE_MARKER="$HOME/.claude/logs/.vault-ingest-done-${TODAY}"
[ -f "$DONE_MARKER" ] && exit 0   # 本日成功済みなら即終了

蒸留を2本(Claude/Codex)に割ったのも同じ理由です。活動の多い日は28時間分のログ消化+記事リライトが40分枠に収まらず連日timeoutし、それが原因でホットキャッシュが凍結していました。サブステップごとに独立マーカーを持たせ、片方がtimeoutしても次スロットが残りだけ再試行します。

捏造させない:ground truth を別に持つ

これがいちばん大事な学びです。claude -p に「会話ログから今日のブリーフを書け」と投げると、ノートに無い予定の拘束時間や所要日数を、それらしく創作することがありました。「M/D〜M/D」という散文を見て「この期間ずっと拘束される」と膨らませてしまう。

対策は2つです。

① カレンダーの実体スナップショットを正典にする。 散文ではなく、Google Calendar APIから取った時刻付きの予定を ground truth として別ファイルに落とし、「これを唯一の正典スケジュールとして読め」と指示します。

② 取得失敗時はlast-known-goodを温存する。 APIが失敗しても、正典を空で潰さない。前回成功時の値を stale 印付きで残します。

text
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に長期記憶を書かせるなら、創作の余地を構造で潰すしかありません。

鮮度判定:古い成果物を「今日の成果」と誤認しない

ブリーフを日付つきでアーカイブするとき、「このランの開始時刻より新しいファイルだけ」を今日の成果と認めます

text
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で判定することで、失敗を成功に偽装しないようにしています。

踏んだ落とし穴

  • launchdから保護領域に書けずサイレント失敗 → TCCプリフライトで早期検知・FDA付与
  • symlinkで保護外に逃がす案 → iCloudが競合処理で壊す。不可
  • スリープ凍結・二重実行 → 複数スロット再試行+mkdirロック+doneマーカー
  • 28h分が1本だと枠超過で連日timeout → ソース別2本+サブ独立マーカー
  • 予定の拘束時間を創作 → カレンダー実体を正典化+last-known-good+推測禁止プロンプト
  • 古い成果物を今日の成果と誤認 → ラン開始スタンプより新しい時だけ完了扱い

まとめ

  • 会話ログ(層1)→ Obsidian(層4)を毎晩・直近28h差分だけ自動蒸留
  • 保護領域はTCCで弾かれる。早期検知で大声、サイレント失敗を許さない
  • スリープ・二重実行は複数スロット再試行+ロック+doneマーカーで自己回復
  • AIに記憶を書かせるならground truthを別に持ち、推測を構造で禁じる
  • mtime鮮度判定で失敗を成功に偽装させない

次回は、この無人ジョブの親玉 ―― Claude Codeの自律ループを24時間回し、コストで暴走を止めるを書きます。

この記事が良かったら

「チップをリクエスト」で著者にチップの受け取り設定をお願いできます

シェア