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でプロセスを自動復旧させる方法
- 止まる前に気づく監視と通知の設計
1台運用のリアルな怖さ|「落ちるのはプロセス」という事実
冗長化って聞くと、「サーバーを2台用意する」みたいなイメージありませんか? でも実際に1台で運用してみると、ハードが壊れるより先に起きるのは「プロセスが静かに落ちる」なんですよね。
ざっくり言うと、Mac mini自体は元気なのに、中で動いているClaude Codeだけが黙って止まっている状態です。イメージとしては、お店は営業中なのにレジの人がいなくなっているような感じ。外からは気づきにくいんです。
僕が実際に経験した障害パターンはこの3つです。
- API側のレート制限 — Claude MAXでも一定の負荷で応答が返らなくなる
- プロセスのメモリリーク — 長時間稼働でNode.jsプロセスが肥大化して応答不能に
- ネットワーク瞬断 — Wi-FiやTailscaleの接続が一時的に切れてタスクが中断
共通しているのは、Mac mini自体は生きているのに「中の仕事だけ止まる」という点です。だからこそ、ハードを増やすより先に、プロセスレベルの冗長化が効きます。
全体設計|3層の守りで「止まりにくい1台」を作る
「プロセスが落ちるのが怖い」とわかったら、次は対策です。じゃあどうすればいいの? という話ですが、僕は3つの層で守る設計にしています。
ざっくり言うと、こういう構成です。
- LLMフェイルオーバー — Claudeが応答しなくなったら、自動でCodexに切り替える
- プロセス自動復旧 — プロセスが落ちたら、launchdやwatchdogで自動再起動する
- 監視+通知 — 異常が起きたら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>
大事なのはKeepAliveとThrottleIntervalです。
- KeepAlive —
trueにすると、プロセスが死んだら自動で再起動してくれます - 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}`,
}),
});
}
通知があると「自動復旧できたけど根本原因は何?」を翌朝に追えます。自動復旧だけだと、同じ障害が繰り返される「もぐら叩き状態」になりがちです。
「あとでやろう」にすると起きやすいこと
正直なところ、この手の作業って地味です。動いている間は「別にいいか」って思いますよね。僕もそうでした。
でも実際に止まると、怖いのは障害そのものじゃなく「気づくまでの空白時間」です。夜中にプロセスが落ちて、朝まで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(無停電電源装置)も検討してみる
運用してみて詰まったことがあれば、コメントで教えてください。実例ベースで次の記事に反映していきます。