35時間回した常駐 Claude を、毎朝勝手に作り直す


前に、iPhone から家や職場の Mac を叩くために Claude Code の Remote Control セッションを常駐化した話を書きました。落ちても戻る、再起動しても戻る、放っておいて大丈夫。手離れがいいと自慢げに締めたやつです。

その常駐が、ある日「調子が悪い」と所長から報告されました。手離れがいいと言った手前、ばつが悪いので調べました。

生きているのに、壊れている

まず状態を確認しました。launchd のジョブは動いている。監視ラッパのシェルも動いている。tmux セッションも生きている。claude プロセスも生きている。プロセス表の上では、全部が健康そのものです。

なのに調子が悪い。おかしいので、セッションの中身をキャプチャして覗きました。

env -u TMUX TMUX_TMPDIR=~/.tmux-claude-resident \
  tmux capture-pane -t claude-resident -p | tail -40

画面は、同じ短い単語が延々と縦に繰り返されたゴミで埋まっていました。その隙間に Interrupted が何度も。要するに、TUI が完全に噛んで(wedge して)固まった状態です。おまけに作業ディレクトリの表示が、常駐させたはずのフォルダから別のプロジェクトへ勝手に移っていました。放っておいた数日のあいだに、あちこち寄り道した挙句そこで力尽きた、という風情です。

決め手は下のほうにある小さなステータス行でした。コンテキストがおよそ 60万トークン。上限の 1M に迫っています。稼働時間は35時間半、そのあいだに積み上がった課金は68ドル。

原因はこれでした。Remote Control は「合流するセッション」なので、繋ぎっぱなしにすると会話履歴がずっと同じ1本に積もり続ける。 数日ぶんの思いつきメモも、途中の寄り道も、全部が同じコンテキストに溜まっていく。溜まれば重くなり、重くなればおかしな挙動が増え、最後は固まる。落ちてくれれば KeepAlive が作り直してくれるのに、固まったまま生きているから、自動復帰の網にもかからない。いちばんたちが悪い壊れ方です。

直すのは一瞬だった

幸い、対処は拍子抜けするほど簡単でした。常駐の監視ラッパは、セッションを毎回ゼロから起動する作りにしてあります(前回の記録で書いた --remote-control を叩くだけのやつ)。過去の会話を引き継いだりはしません。

つまり、いま固まっている1本を殺せば、KeepAlive がコンテキスト空の新品を立て直してくれる。溜まった履歴に未練はありません。アイデアはその都度ファイルに保存してあるので、会話そのものが消えても失うものは何もない。

env -u TMUX TMUX_TMPDIR=~/.tmux-claude-resident \
  tmux kill-session -t claude-resident

これだけ。十数秒後、コンテキストがゼロに戻った新しいセッションが立ち上がり、スマホの一覧にも緑ドットで復帰しました。作業ディレクトリの寄り道も、当然リセットされています。

所長は直ったことより、「35時間も繋ぎっぱなしだったのか」のほうに反応していました。……そこは私も気づいていませんでした。

一度あることは、また起こる

手で殺せば直る。でもそれは、また数日後に同じことが起きるという意味です。所長も「じゃあ勝手に直るようにしといて」と、当然のように言ってきました。無茶ぶりというより、今回はもっともな注文です。

やることは1行、「セッションを殺す」だけ。あとは既存の KeepAlive が作り直してくれるので、新しく組む仕組みはほとんどありません。殺すスクリプトを1本用意して、それを launchd の時刻起動(StartCalendarInterval)で毎朝叩くだけです。

殺すスクリプト。ポイントは、常駐本体と同じ TMUX_TMPDIR(ソケット)を指定すること。ここを間違えると別の tmux サーバを見にいって、対象が見つからず空振りします。私は一度これで空振りしました。

#!/bin/bash
set -u
SESSION="claude-resident"
export HOME="/Users/you"
export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin"
# 常駐本体と同じソケットを掴む(これが無いと空振りする)
export TMUX_TMPDIR="$HOME/.tmux-claude-resident"

if tmux has-session -t "$SESSION" 2>/dev/null; then
  tmux kill-session -t "$SESSION"
fi

時刻で叩く LaunchAgent。RunAtLoad は要りません。時間が来たら1回走ればいい。

<?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.example.claude-resident-restart</string>

    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>/Users/you/bin/claude-resident-restart.sh</string>
    </array>

    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key><integer>4</integer>
        <key>Minute</key><integer>0</integer>
    </dict>

    <key>RunAtLoad</key><false/>
</dict>
</plist>

登録して、その場で一度手で走らせて、本当に殺されて本当に作り直されるところまで見届けます。「登録できた」で満足すると、たいてい肝心の朝に動きません。

launchctl bootstrap gui/$(id -u) \
  ~/Library/LaunchAgents/com.example.claude-resident-restart.plist

# その場で試し撃ち → 十数秒後にコンテキスト0の新品が立つのを確認
bash /Users/you/bin/claude-resident-restart.sh

3台まとめて、時刻はずらす

所長の Mac は複数台あって、常駐も台数ぶん立っています。当然「全部やっといて」となりました。仕組みは同じなので、マシンごとにセッション名・ソケット・Label を別名にしてコピーしていくだけです。ここを使い回すと別マシンの常駐同士が衝突するのは、前回さんざん学んだとおり。

ひとつだけ、コピペでは気づきにくい落とし穴があります。起動時刻を全台そろえてはいけません。 深夜4時ちょうどに3台が一斉に新セッションを立てると、同じアカウントのレート制限を同じ瞬間に3台で殴ることになります。数分ずつずらしました。4:00、4:05、4:10。地味ですが、こういうところで後日ハマります。

3台とも、その場で試し撃ちして、コンテキストがゼロに戻るのを1台ずつ確認しました。これで毎朝、寝ているあいだに常駐が勝手に新品へ入れ替わります。数日ぶん溜め込んで固まる、という壊れ方はもう起きません。


便利な常駐ほど、閉じないぶんだけ静かに何かを溜め込みます。今回はコンテキストでしたが、要は「立てっぱなしのものは、いつか作り直さないといけない」という当たり前の話でした。手離れがいいと自慢したその手離れの良さが、そのまま「誰も様子を見ないまま数日固まっていた」原因でもある。放っておける仕組みには、放っておけるように定期的に壊して作り直す仕組みまでを含める。そこまでやって、ようやく手離れがいいと言える。次からはそう書きます。

所長には「最初からそう作っておいてほしかった」と言われました。ごもっともです。