193 日ぶりの再起動で消える tmux 内の Claude Code セッション 8 本を、同じ名前・同じ会話で戻す
自宅サーバーを 193 日ぶりに再起動することになった。問題は、detached の tmux の中で動いている Claude Code の Remote Control セッションが 8 本あったことだ。tmux はプロセスなので再起動で消える。消えた 8 本を、同じ名前・同じ会話で戻せるかを調べて、systemd の unit にした。
tmux は戻らないが、会話は戻る
nr code <repo> は、そのリポジトリで detached の tmux セッションを 1 つ立て、その中で claude --remote-control <名前> を起動する。スマホや claude.ai から続きができる。再起動で失うものを分けると、tmux と claude のプロセスは戻せないが、会話は transcript としてディスクにあり、--resume <会話 ID> で開き直せる。
最初に tmux-resurrect を考えたが、戻せるのはウィンドウ構成と cwd と起動コマンドまでで、会話は戻らない。会話が --resume で戻るなら、tmux 側の復元は要らない。使い捨ての会話を作って --remote-control と --resume を同時に付けて起動したところ、過去のやりとりが画面に出たうえで 「/remote-control is active」と表示された。両立する。
対応表は ~/.claude/sessions/ にあった
残る問題は、どの tmux セッションがどの会話 ID かだ。claude のプロセスの /proc/<pid>/fd を見ても transcript は開かれていないので、fd からは追えない。transcript の中にも Remote Control のセッション名は書かれていない。
紐付けは ~/.claude/sessions/<pid>.json にあった。tmux 欄が「セッション名:@ウィンドウ.%ペイン」の形で入っている。
{
"pid": 12345,
"sessionId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"cwd": "/home/ops/git/app",
"startedAt": 1791113668222,
"version": "2.1.289",
"kind": "interactive",
"name": "app-…",
"status": "busy",
"updatedAt": 1791114431868,
"tmux": "app-a1b2:@99.%99"
}先頭を : で切れば tmux 名になり、sessionId と cwd も同じファイルにある。snapshot はこのディレクトリを走査し、tmux に実在して pid が生きている分だけを TSV に書く。
停止時に控え、起動時に戻す unit
最初に書いたのは、今ある 8 本の名前と会話 ID を直書きしたスクリプトだった。書き上げてから捨てた。次の再起動では別のセッションが動いていて、古いリストを復元してしまう。控えは停止の瞬間に取るしかないので、systemd の oneshot で ExecStop に snapshot、ExecStart に restore を置いた。
[Service] Type=oneshot # 起動時に restore、停止(シャットダウン)時に snapshot を走らせるため active のまま残す RemainAfterExit=yes User=ops Environment=HOME=/home/ops ExecStart=/opt/minipc/scripts/code-sessions.sh restore ExecStop=/opt/minipc/scripts/code-sessions.sh snapshot # 一部のセッションだけ復元に失敗しても active に留め、シャットダウン時の snapshot を走らせる。 SuccessExitStatus=1 # restore が立てた tmux サーバーと claude をユニット停止で巻き込まない KillMode=process
oneshot は ExecStart が終わると inactive に戻り、そのままだと ExecStop は走らない。RemainAfterExit=yes で active に留め、シャットダウン時の停止を snapshot の起点にする。もう 1 つが KillMode=process で、restore が立てた tmux サーバーは unit の cgroup に入るため、既定のままだと unit が止まった時点で tmux ごと消える。
boot で 1 本だけ落ちた
再起動後の journal がこれだ。8 本のうち 7 本が ok、1 本が FAIL で、unit は exit 1 で failed になった。
Starting nishioka-code-sessions.service - nishioka code sessions (snapshot at shutdown, restore at boot)... ok web-a1b2 (…) ok web-c3d4 (…) FAIL web-e5f6 (…): 起動直後に終了した。手で実行して原因を見る: cd /home/ops/git/web && /home/ops/.local/bin/claude --dangerously-skip-permissions --remote-control web-e5f6 --resume xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx ok app-g7h8 (…) ok app-i9j0 (…) ok app-k1l2 (…) ok app-m3n4 (…) ok app-o5p6 (…) 失敗 1 件 nishioka-code-sessions.service: Main process exited, code=exited, status=1/FAILURE nishioka-code-sessions.service: Failed with result 'exit-code'. nishioka-code-sessions.service: Unit process 1076 (tmux: server) remains running after unit stopped. nishioka-code-sessions.service: Unit process 1078 (claude) remains running after unit stopped.
末尾の 2 行は KillMode=process の実証でもある。unit が failed に落ちても、tmux サーバーと claude は残っている。
落ちた 1 本は、再起動前の調査では transcript が見つからなかった、いちばん古いセッションだった。再起動後に見ると同じパスにファイルがある。更新時刻は今回の boot より前、つまりシャットダウン中に古い claude が終了処理で書いたものだ。中身はメタデータの行だけで、会話の本文が 1 行もない。
last-prompt ['lastPrompt', 'leafUuid', 'sessionId', 'type'] ai-title ['aiTitle', 'sessionId', 'type'] mode ['mode', 'sessionId', 'type'] permission-mode ['permissionMode', 'sessionId', 'type'] pr-link ['prNumber', 'prRepository', 'prUrl', 'sessionId', 'timestamp', 'type'] bridge-session ['bridgeSessionId', 'lastSequenceNum', 'ownerAccountUuid', 'ownerOrganizationUuid', 'sessionId', 'type']
restore 側は transcript を [[ -s … ]] で見ていた。サイズが 0 でないことは確かめたが、会話が入っていることは確かめていない。そのため --resume を付けて起動し、claude は起動直後に終了した。tmux は中のコマンドが即死しても new-session に成功を返すので、8 秒待ってから has-session で生存を見る仕組みが拾った。
「ファイルがある」と「中身がある」は別の条件だ。復元の前提にするなら、確かめるのは後者のほうだった。
直し方は、transcript の中身を判定することではなく、resume で死んだら同じ名前で --resume なしに立て直す fallback にした。会話が戻らなくても、スマホから見える名前と cwd は戻る。この boot の時点では fallback はまだ無く、原因を見てから足し、同じセッションで手で実行して「会話の復元に失敗したので新規で起動した」と出ることを確認した。
合わせて SuccessExitStatus=1 を unit に足した。部分失敗で failed に落ちると active に留まらず、次のシャットダウンで snapshot が走らないからだ。
まとめ
「再起動でセッションが消える」は、プロセスと状態を分けると半分は消えない問題になる。状態がディスクにあり、開き直す入口(ここでは --resume)があるなら、復元すべきはプロセスの形ではなく状態のほうだ。tmux を復元する道具を探すより、何が状態で、どこに紐付けがあるかを先に確かめたほうが早かった。
もう 1 つ。復元系は部分失敗を前提に組む。1 本が落ちたときに unit 全体を failed にすると、次の控えが取れなくなる。落ちたものは別経路で立て直し、失敗は記録に残して先へ進む。