Mac mini AIエージェント冗長化術|Claude/Codex自動切替

Mac miniでAIエージェントを動かしていて、「もしプロセスが落ちたら全部止まるんだよな…」って思ったことありませんか? 僕はしょっちゅうあります。この記事は、Mac mini 1台でもAIエージェントを止まりにくくしたい人向けです。読むと、Claude CodeとCodexの自動切替、プロセスの自動復旧、止まる前に気づく監視の3つが作れます。僕は福祉事業のIT全般を担当しながら、個人でもAI開発をしているので、できるだけ再現しやすい形でまとめました。

こんな方におすすめ

  • Mac mini 1台でAIエージェントを24時間動かしている方
  • Claude CodeやCodexが落ちたとき、手動で再起動している方
  • 2台買うのは厳しいけど、止まりにくい仕組みは欲しい方

この記事でわかること

  • 1台運用で実際に起きやすい障害パターン
  • Claude → Codex 自動フェイルオーバーの作り方
  • launchdとwatchdogでプロセスを自動復旧させる方法
  • 止まる前に気づく監視と通知の設計
僕は普段、福祉事業のIT全般を担当しつつ、AI×SaaS開発を複業で進めています。Mac mini M4 Proを24時間稼働させていて、重視しているのは「止まらない仕組み」と「少ない工数で回る運用」です。

1台運用のリアルな怖さ|「落ちるのはプロセス」という事実

冗長化って聞くと、「サーバーを2台用意する」みたいなイメージありませんか? でも実際に1台で運用してみると、ハードが壊れるより先に起きるのは「プロセスが静かに落ちる」なんですよね。

ざっくり言うと、Mac mini自体は元気なのに、中で動いているClaude Codeだけが黙って止まっている状態です。イメージとしては、お店は営業中なのにレジの人がいなくなっているような感じ。外からは気づきにくいんです。

僕が実際に経験した障害パターンはこの3つです。

  • API側のレート制限 — Claude MAXでも一定の負荷で応答が返らなくなる
  • プロセスのメモリリーク — 長時間稼働でNode.jsプロセスが肥大化して応答不能に
  • ネットワーク瞬断 — Wi-FiやTailscaleの接続が一時的に切れてタスクが中断

共通しているのは、Mac mini自体は生きているのに「中の仕事だけ止まる」という点です。だからこそ、ハードを増やすより先に、プロセスレベルの冗長化が効きます。

全体設計|3層の守りで「止まりにくい1台」を作る

「プロセスが落ちるのが怖い」とわかったら、次は対策です。じゃあどうすればいいの? という話ですが、僕は3つの層で守る設計にしています。

ざっくり言うと、こういう構成です。

  1. LLMフェイルオーバー — Claudeが応答しなくなったら、自動でCodexに切り替える
  2. プロセス自動復旧 — プロセスが落ちたら、launchdやwatchdogで自動再起動する
  3. 監視+通知 — 異常が起きたらDiscordに即通知。気づかないまま放置を防ぐ

イメージとしては、サッカーの守備に似ています。第1層がGK(LLM切替)、第2層がDF(自動復旧)、第3層がMF(監視通知)。1つ抜かれても次の層が止めてくれます。

守る対象 仕組み 復旧時間の目安
第1層 LLM応答 Claude → Codex 自動切替 数秒
第2層 プロセス死活 launchd / watchdog 10〜30秒
第3層 全体の異常 監視スクリプト + Discord通知 気づき次第

この3層をセットで入れると、「気づいたら止まってた」がほぼなくなります

第1層:Claude → Codex 自動フェイルオーバーの実装

まず一番効果が大きい、LLMの自動切替から作ります。「Claude Codeにお願いしたけど応答が返ってこない…」って経験ありませんか? そんなとき、自動でCodexに切り替わったら安心ですよね。

考え方はシンプルです。Claudeにリクエストして、タイムアウトかエラーが返ったら、同じタスクをCodexに投げ直す。レストランで注文して料理が来なかったら、隣のお店に行くのと同じです。

/** LLMフェイルオーバー付きでタスクを実行する */
async function executeWithFallback(task: string): Promise<string> {
  try {
    // まずClaudeで実行(タイムアウト120秒)
    return await executeWithClaude(task, { timeout: 120_000 });
  } catch (error) {
    console.warn('Claude failed, falling back to Codex:', error.message);
    // Claudeが失敗したらCodexにバトンタッチ
    return await executeWithCodex(task, { timeout: 120_000 });
  }
}

ポイントは3つあります。

  • タイムアウトを必ず設定 — 無限に待つと切替が発動しません。僕は120秒にしています
  • リトライは1回まで — 何度もリトライすると、APIコストが跳ね上がります
  • 切替ログを残す — いつ、なぜ切り替わったかを記録しておくと、後で原因を追えます

Codex CLIはnpm i -g @openai/codexで導入できます。Claude MAXとChatGPT Proの両方を契約していれば、追加コストなしでフェイルオーバー先を確保できます

第2層:launchdとwatchdogでプロセスを自動復旧

LLMの切替ができても、そもそもNode.jsプロセス自体が落ちたら意味がないですよね。ここを守るのが第2層です。

macOSにはlaunchdという仕組みがあります。ざっくり言うと「このプロセスが落ちたら、自動で再起動してね」とOSにお願いできる機能です。Windowsのサービスに近いイメージです。

<!-- ~/Library/LaunchAgents/com.masu.agent.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.masu.agent</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/local/bin/node</string>
    <string>/path/to/your/agent/dist/index.js</string>
  </array>
  <key>KeepAlive</key>
  <true/>
  <key>ThrottleInterval</key>
  <integer>10</integer>
  <key>StandardOutPath</key>
  <string>/tmp/masu-agent.log</string>
  <key>StandardErrorPath</key>
  <string>/tmp/masu-agent-error.log</string>
</dict>
</plist>

大事なのはKeepAliveThrottleIntervalです。

  • KeepAlivetrueにすると、プロセスが死んだら自動で再起動してくれます
  • ThrottleInterval — 再起動の間隔。10秒にしておくと、クラッシュループ(落ちて→起動して→すぐ落ちての繰り返し)を防げます

登録はターミナルから1行です。

launchctl load ~/Library/LaunchAgents/com.masu.agent.plist

注意点がひとつ。launchdはプロセスの「死」は検知できますが、「フリーズ」は検知できません。プロセスが生きているけど応答しない状態には、別途ヘルスチェックが必要です。そこで役立つのがwatchdogスクリプトです。

#!/bin/bash
# watchdog.sh — ヘルスチェック付きプロセス監視
HEALTH_URL="http://localhost:3000/health"
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$HEALTH_URL")

if [ "$RESPONSE" != "200" ]; then
  echo "$(date): Health check failed (HTTP $RESPONSE), restarting..."
  launchctl kickstart -k gui/$(id -u)/com.masu.agent
fi

これをcronで5分おきに回せば、フリーズ状態も拾えます。

第3層:止まる前に気づく監視とDiscord通知

ここまでで「自動切替」と「自動復旧」はできました。でもこれだけだと「そもそも異常が起きていたこと」に気づけないんですよね。朝起きて「あれ、夜中に3回も再起動してたの?」みたいな。

だから第3層では、異常が起きたらDiscordに通知を飛ばします。僕が監視しているのは3つの指標です。

  • プロセス再起動回数 — 1時間に2回以上再起動したら異常
  • LLMフォールバック回数 — Claude → Codex切替が頻発していたらAPI側の問題
  • ヘルスチェック連続失敗 — 3回連続で失敗したらフリーズの疑い
/** Discord Webhook で異常通知を送る */
async function notifyDiscord(message: string): Promise<void> {
  await fetch(process.env.DISCORD_WEBHOOK_URL!, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      content: `⚠️ [Agent Monitor] ${message}`,
    }),
  });
}

通知があると「自動復旧できたけど根本原因は何?」を翌朝に追えます。自動復旧だけだと、同じ障害が繰り返される「もぐら叩き状態」になりがちです。

通知先はDiscordがおすすめです。Webhookだけで完結するので、外部サービスの契約が不要です。Slackでも同じ仕組みで動きます。

「あとでやろう」にすると起きやすいこと

正直なところ、この手の作業って地味です。動いている間は「別にいいか」って思いますよね。僕もそうでした。

でも実際に止まると、怖いのは障害そのものじゃなく「気づくまでの空白時間」です。夜中にプロセスが落ちて、朝まで8時間止まっていた。その間にスケジュールされていたタスクが全部飛んでいた。手動で再起動して、溜まったタスクを手作業でリカバリ…。

僕はこれで半日潰れたことがあります。3層の仕組みを入れたあとは、同じ障害が起きても自動復旧+通知で済むようになりました。朝起きてDiscordを確認するだけです

この記事を書いている理由

僕自身、Mac mini M4 Pro(24GB)を24時間稼働させています。Node.js + TypeScriptのモノレポをtmuxで管理し、リモートはTailscale、開発は「Ghostty + tmux + Claude Code」が中心です。

冗長化の記事を調べると、だいたい「サーバーを2台以上用意して…」から始まります。でも個人開発や小規模運用だと、2台目を買うのはハードルが高い。だったら1台でできることを先にやろう、という発想で書きました。僕が詰まったポイントを先に共有して、同じ状況の人の遠回りを減らしたい。それがこの記事を書いた理由です。

今日からできるアクション

  • まずlaunchdのKeepAlive設定を入れて、プロセス自動復旧を有効にする
  • LLMフェイルオーバー(Claude → Codex)をtry-catchで実装する
  • watchdogスクリプトを作って、Discord通知を繋いでみる
  • 物理対策として放熱スタンドUPS(無停電電源装置)も検討してみる

運用してみて詰まったことがあれば、コメントで教えてください。実例ベースで次の記事に反映していきます。