Obsidian頭脳化計画:AIエージェントの外部記憶をObsidian Vaultで実現する

はじめに

最近はやりのObsidianで外部脳を作ろうというkarpathyさんのアイデアをちょっと違う形でAI Agent向けの外部脳を作ってみました。私は、ClaudeCodeがメインでOpenCalw、kiro、Antigravity、OpenCladeなど。。。節操なく使ておりり、各エージェント事にコンテキストがバラバラという課題がありまして、これをNAS+Obsidianで効率的に共有できないかと考えました。 私はRedmineでプロジェクト管理しているので、Redmineをベースに、Obsidian Vaultを全AIエージェント共通の外部記憶として整備するプロジェクト「redobrain」を始めました。

何を作ったのか

一言で言うと、Redmineに書いた情報を自動的にObsidian Vaultに取り込み、AIが読みやすい形に統合するパイプラインです。基本的にはOpenClawのSKILLSで、スクリプトを実行させたりマージはLLMにやらせてます。この仕組み自体、私がアイデアを出しClaudeCodeで作成したものです。

人間 → Redmineにチケットを書く
         ↓ (自動同期)
    Sources/Redmine/ に1チケット1ファイルで保存
         ↓ (ルールベース分類)
    classify-state.json に分類結果を記録
         ↓ (OpenClawが直接マージ)
    Merged/ に統合ドキュメントとして出力
         ↑
    他のAI Agentが ENTRY.md 経由で参照

人間がやることは「Redmineにチケットを書く」だけ。あとは全自動です。

設計原則:Sourcesが唯一の真実

このシステムの核となる設計原則は:

Sources が唯一の真実。Merged はビルド成果物。

プログラマーなら馴染みのある考え方ですね。ソースコードとビルド成果物の関係と同じです。

  • Sources/ = ソースコード(Redmineチケット、手動投入ドキュメント)
  • Merged/ = ビルド成果物(AIが読む統合ドキュメント)
  • いつでも Sources/ から Merged/ を再生成できる

これにより「マージ済みだからスキップ」という判断が不要になります。ルールが変わった、情報が更新された → 再生成すればいい。シンプルです。

2フェーズ・アーキテクチャ

Phase 1: Classify(ルールベース、LLM不要)

分類はLLMを使いません。純粋なルールベースです。

  • Redmineチケットの parent_id を辿ってルートチケットを特定
  • ルートチケット = コアドキュメント(統合先)
  • 子チケットは親のコアドキュメントに分類される
def _find_root(self, ticket_id, parent_map):
    """parent_idチェーンを辿ってルートを見つける(循環検出付き)"""
    visited = set()
    current = ticket_id
    while current in parent_map and parent_map[current] is not None:
        if current in visited:
            break
        visited.add(current)
        current = parent_map[current]
    return current

最初は分類もOpenClawにやらせようと思ったのですが、いまいちだったのでRedmineのプロジェクトをベースにClaudeCodeで先に仕分けルールを作らせました。

Phase 2: Merge(OpenClawが直接実行)

SKILLS.mdでスクリプト実行とその結果で判断させ、マージの仕方を指示しています。

## マージ作業(あなたが直接行う — スクリプト不要)

**重要**: マージは2フェーズに分けて行う。1回の処理を小さく保つこと。

### 「マージして」と言われたら即座にこれを実行する

**⚠️ 絶対に聞き返さない。追加情報を求めない。以下をそのまま実行する。**

何をマージするか、どこにマージするかは聞かなくてよい。classify コマンドが自動判定する。

```bash
# Step 1: 分類(自動)— 何をマージすべきか自動判定される
cd ~/.openclaw/workspace/skills/redobrain
set -a; source systemd/openclaw.env; set +a
python3 -m scripts.main --config /mnt/share/Obsidian/_openclaw/config.yaml classify
```

```bash
# Step 2: 次のマージ対象を確認 — これが「何をマージするか」の答え
python3 -m scripts.main --config /mnt/share/Obsidian/_openclaw/config.yaml next-merge
```

**Step 2 の出力を見て、そこに書かれた core_doc と sources に従って作業する。**
ユーザーに何も聞く必要はない。

Step 3以降はあなたが直接ファイルを読み書きして行う:

**⚠️ 1回のセッションで処理するのは1コアドキュメントのみ。**

```
Step 3: next-merge の出力を確認 → core_doc と sources 一覧を把握する
Step 4: その core_doc に属する Sources を1件ずつ読む(一度に全部読まない)
Step 5: 内容を理解し、コアドキュメントを作成・更新する
         - 既存ファイルがあれば先に Archive に退避
           cp /mnt/share/Obsidian/Merged/xxx.md /mnt/share/Obsidian/Archive/Merged/xxx_YYYY-MM-DD_HHmm.md
         - /mnt/share/Obsidian/Merged/{category}/{name}.md に書き込む
Step 6: マージ完了したら mark-merged で記録する
         python3 -m scripts.main --config /mnt/share/Obsidian/_openclaw/config.yaml mark-merged "Merged/xxx/yyy.md"
Step 7: Discord に完了を報告する(例:「Merged/Trading/daytrade-bot.md をマージしました」)
Step 8: /mnt/share/Obsidian/00-Entry/ENTRY.md を更新する
         - 統合ドキュメント一覧のパスは Obsidian wiki-link で記載する
         - 例: `[[Merged/Trading/daytrade-bot]]` (.md は省略)
         - テーブル形式: `| タイトル | [[Merged/Trading/daytrade-bot]] | 概要 | 日付 |`
Step 9: /mnt/share/Obsidian/_openclaw/logs/YYYY-MM-DD.md にログを記録する
```

Vault構造

/mnt/share/Obsidian/
├── 00-Entry/
│   └── ENTRY.md              ← 全AIエージェントの入口
├── Sources/
│   ├── Redmine/              ← チケットごとに1ファイル
│   └── Other/                ← 手動投入ドキュメント
├── Merged/                   ← 統合ドキュメント(カテゴリ別)
│   ├── Trading/
│   ├── Projects/
│   ├── Infrastructure/
│   └── ...
├── Archive/
│   └── Merged/               ← 過去バージョン(自動退避)
└── _openclaw/
    ├── config.yaml
    ├── merge_rules.yaml      ← 分類ルール定義
    ├── classify-state.json   ← 分類結果キャッシュ
    └── logs/

ポイントは 00-Entry/ENTRY.md の存在です。どのAIエージェントも、このファイルを1つ読めば全情報にアクセスできる。設定ファイルに1行追加するだけ:

プロジェクト情報は /mnt/share/Obsidian/00-Entry/ENTRY.md を参照。

実装の工夫

増分処理

毎回全チケットを再分類するのは無駄なので、classified_atlast_synced を比較して変更があったものだけ処理します。

# 増分処理: 前回分類済みで変更なしならスキップ
existing = sources.get(source_path)
if existing and existing.get("classified_at", "") >= last_synced:
    continue

アーカイブ自動退避

マージ前に既存の Merged/ ファイルがあれば、自動的に Archive/ にタイムスタンプ付きでコピーします。git的な発想ですが、Obsidianのプレーンマークダウンという制約の中では十分実用的です。

ロックファイル

NAS上のVaultに複数プロセスが同時アクセスしないよう、シンプルなロックファイル機構を実装しています。タイムアウト付きで、デッドロックも防止。

技術スタック

  • Python 3.11+ — パイプラインスクリプト
  • OpenClaw — LLMエージェント基盤(Qwen3.6-35b-a3b on Ollama)
  • Redmine — 情報の入力元(人間が書く場所)
  • Obsidian — 情報の出力先(AIが読む場所)
  • Discord — 承認フロー・通知
  • NAS (SMB/NFS) — Vault の物理ストレージ(複数マシンからアクセス可能)

技術スタック

利用環境

  • Windows 11 (Claude Code/kiro/Antigravity) 普段使うPC
  • Ubuntu Linux 24.04(Openclaw/Claude Code/kiro-cli) Openclawマシン
  • Mac mini(まだ届いてない(笑))

現在の稼働状況

  • Redmine sync: 稼働中(329チケット同期済み)
  • Classify: 稼働中(273ソース分類済み → 157コアドキュメント)
  • Merge: OpenClawによる実行が進行中
  • systemdタイマーで15分ごとに自動実行

なぜObsidianなのか

「なぜNotionやConfluenceではなくObsidianか?」という疑問があるかもしれません。理由は明確です:

  1. プレーンマークダウン — APIもDBも不要。ファイルを読むだけ
  2. ローカルファイルシステム — AIエージェントが直接アクセスできる
  3. NASマウント — 複数マシン(Windows/Linux/Mac)から同じVaultにアクセス
  4. Obsidianのwiki-link — ドキュメント間のリンクが簡潔に書ける
  5. 人間も読める — Obsidianアプリで普通にブラウズ・編集できる

AIエージェントにとって最も低摩擦なインターフェースは「ファイルを読む」こと。それ以上でもそれ以下でもない。

今後の展望

  • マージ品質の改善(現在はOpenClawの出力をそのまま使っているが、フォーマットの一貫性にばらつきがある)
  • 定期的な全再マージ(ルール変更時に一括再生成)
  • 他のAIエージェント(Claude Code、Kiro等)からの参照実績を積む
  • Sources/Other/ への手動投入ワークフローの整備

まとめ

redobrainは「AIエージェントに長期記憶を持たせる」という課題に対する、実用的な解答です。

設計のポイントは:

  • 人間はRedmineに書くだけ(既存ワークフローを変えない)
  • 分類はルールベース(LLMの気まぐれに依存しない)
  • 統合はLLMが直接実行(エージェントの強みを活かす)
  • 出力はプレーンマークダウン(どのAIからも読める)

ぶっちゃけ、これClaude Codeに課金してやらせたら簡単に終わると思います。それをケチって(いや、実験として)OpenClaw + Ollamaで実行させるためには、JOBを細かく切るとかちょっとお馬鹿でも実行できるようにするとか一部、スクリプトを作るとかってことをしてる訳ですね。まぁ、不毛っちゃ不毛です(笑)

環境: OpenClaw v2026.5.4 / Ollama 0.20.5 / Qwen3.6-35b-a3b / RTX 3060 12GB / Ryzen 5600X / RAM 32GB / Obsidian on NAS (SMB)

OpenClaw+Ollamaでどうしてもフェールオーバーする

はじめに

前回の記事では、Qwen3.6-35b-a3bでOpenClawが実用化したと報告しました。しかし運用を続けていると、タイムアウト問題という壁にぶつかりました。

今回は、OpenClawのタイムアウト設定を調査した結果、設定では解決できないハードコードされたバグに行き着いた顛末を記録します。

症状:120秒で勝手にフォールバックする

OpenClawのログに以下のエラーが頻発するようになりました:

10:50:13 error diagnostic lane task error:
  lane=session:agent:main:discord:channel:...
  durationMs=122139
  error="FailoverError: LLM request timed out."

timeoutSeconds: 600(10分)に設定しているのに、約122秒(2分)でタイムアウトしてフォールバック先のOpenRouterに切り替わってしまう。

調査その1:設定は効いているのか?

まず timeoutSeconds を600→900に変更して様子を見ました。すると別のログが出現:

11:36:55 embedded run timeout:
  timeoutMs=600000
  durationMs=600667

こちらは正しく600秒でタイムアウトしている。つまり timeoutSeconds の設定自体は効いている

では122秒のタイムアウトは何なのか?

調査その2:Ollamaのログを確認

Ollamaのjournalctlログを確認すると、決定的な証拠が見つかりました:

12:00:54 loading model: KvSize:131072, GPULayers:15/41
12:00:59 model weights: GPU=7.6GiB, CPU=14.7GiB
12:01:10 llama runner started in 16.28 seconds
12:02:53 [GIN] 500 | 1m59s | POST "/api/chat"

Ollama側は処理中なのに、クライアント(OpenClaw)が約2分で接続を切っている。 Ollamaは500エラーを返しているが、これはクライアント切断が原因。

調査その3:2種類のタイムアウトが存在する

ログを時系列で整理すると、全体像が見えてきました:

タイムアウト 設定 効果
embedded run timeout agents.defaults.timeoutSeconds: 900 エージェント実行全体の制限時間。設定可能
LLM HTTP fetch timeout ハードコード(~120秒) 個別LLMリクエストの待機時間。設定不可

timeoutSeconds はエージェントの「1回の実行全体」のタイムアウト。一方、個別のHTTPリクエスト(OllamaのAPIを叩く1回のfetch)には、別のハードコードされたタイムアウトが存在していました。

なぜ120秒で切れるのか

35B MoEモデルをCPU/GPU分割(15/41レイヤーのみGPU)で131Kコンテキストで動かすと、prefill(プロンプト処理)だけで120秒以上かかることがあります。

ストリーミングモードは有効ですが、prefill中は最初のトークンすら生成されないため、HTTPレスポンスとして1バイトも返りません。OpenClawのHTTPクライアントは「レスポンスが来ない」と判断してabortします。

timeoutMs を試したが…

OpenClawのGitHub Issuesで timeoutMs というパラメータの要望を見つけたので試してみました:

"models": {
  "providers": {
    "ollama": {
      "timeoutMs": 900000
    }
  }
}

結果:

config reload skipped (invalid config):
- models.providers.openrouter: Unrecognized key: "timeoutMs"
Config auto-restored from last-known-good

v2026.5.4では timeoutMs は未実装。 設定に入れるとバリデーションエラーで拒否され、設定全体がロールバックされます。

OpenClawの既知バグだった

GitHub Issuesを調査すると、同じ問題が複数報告されていました:

  • #61487 — “LLM HTTP request timeout hardcoded at ~60s”
  • #46049 — “LLM request timeout ignores configured timeout settings”
  • #45637 — “Add timeoutMs config support for LLM Providers”

ソースコード上は DEFAULT_TIMEOUT_MS$2 = 3e4(30秒)がハードコードされているとの報告もあり、バージョンによって30秒〜120秒の範囲で異なるようです。

結論:現時点でユーザーが変更する手段はない。

ワークアラウンドの検討

対策 有効性 理由
timeoutSeconds を伸ばす embedded run全体には効くが、個別リクエストには効かない
timeoutMs を設定する 未実装。設定するとconfig拒否
モデルを常時ロード 問題は稼働中に発生。ロード済みでもprefillが遅い
コンテキストを下げる 128K未満ではエージェントとして実用にならない
ストリーミングで回避 既にstream:true。prefill中はデータが返らないので無意味
フォールバックに任せる OpenRouterが成功するので実用上は動く

現状の運用

結局、フォールバックが正しく動いているので、実用上は問題なく動作しています:

  1. Ollama(qwen36-35b-agent)にリクエスト
  2. ~120秒でタイムアウト → abort
  3. OpenRouter(deepseek-v4-flash)にフォールバック → 成功

ただし、ローカルモデルが使われる頻度が下がり、OpenRouterのAPI費用が発生するのが残念なポイントです。

まとめ

  • agents.defaults.timeoutSeconds はエージェント実行全体のタイムアウト。個別HTTPリクエストには効かない
  • OpenClawのLLM HTTPクライアントには~120秒のハードコードタイムアウトがある(バグ)
  • timeoutMs パラメータは未実装(Feature Requestとして存在するのみ)
  • 35B MoE + 131Kコンテキスト + CPU/GPU分割の環境では、prefillだけで120秒を超えることがある
  • 根本修正はOpenClaw側のアップデート待ち

教訓: ローカルLLMでOpenClawを使う場合、「モデルの推論速度」だけでなく「prefill時間(最初のトークンが出るまでの時間)」も重要な指標です。VRAM不足でCPUオフロードが多いほどprefillが遅くなり、ハードコードタイムアウトに引っかかりやすくなります。


環境: OpenClaw v2026.5.4 / Ollama 0.20.5 / RTX 3060 12GB / Ryzen 5600X / RAM 32GB