コンテンツにスキップ

restore — 止まったセッションを同じ会話で立て直す

ccs restore                      戻せるものを見せる(既定。何もしない)
ccs restore --yes                戻す
ccs restore <slug>...            対象を名指しする
ccs restore <slug> --list        その場所に残っている会話を並べる
ccs restore <slug> --session-id <uuid> --yes
                                 古い会話を名指しで戻す
ccs restore --last               前回の停止まで生きていた組だけに絞る
ccs restore --since <期間>       その期間だけ遡る(例: 6h)
ccs restore --all                古い会話も候補に入れる

何が起きているのか

PC を再起動すると tmux サーバごと消える。ccs hub up を自動起動に入れてあれば hub は戻ってくるが(hub を常時立てる)、 他のセッションは誰も立て直さない。 アプリの一覧からは消えたままになる。

消えたのはセッションであって、会話ではない。会話は

~/.claude/projects/<エンコードした cwd>/<uuid>.jsonl

に残っているので、同じ場所で claude --resume <uuid> を立てれば同じ会話が戻る。 ccs restore はそれを候補ごと見つけてまとめてやる。

ccs は状態ファイルを持たず、立っているものは tmux から導出している (design.md §2.1)。 だから「さっきまで何が立っていたか」は誰も覚えていない。 覚える代わりに、 ccs 自身が場所を決めているところだけを舐めて、そこに会話ログがあるかを見る。

# 何 見るもの
1 止まったペイン cc/ の tmux セッションは残っているが claude が死んでいる
2 消えた作業枠 ~/.cc-scratch/<n> の会話ログ
3 消えた worktree ghq 配下の <repo>/.worktrees/<branch> の会話ログ
4 消えたリポジトリのセッション ghq 配下で、ccs が立てた印のある会話ログ

4 だけ条件が 1 つ多い。 ghq 配下の会話ログは ccs が立てたものとは限らず、 デスクトップアプリや VS Code から開いたセッションも同じ場所に溜まる。素朴に拾うと ccs が管理していない会話まで tmux に生えるので、印のあるものだけを拾う。

印とは

ghq 配下では、これは印ではなく「痕跡」。 ccs の印(.ccs.json)は使い捨て作業枠にしか置かない ── git の作業ツリーに置くと git status に出て、コミットされ得て、clean 判定を壊す (ADR-0002 決定 5)。だから ghq 配下で読めるのは、 名前が付いていたことの痕跡だけ。

ccs new は claude -n <slug> で名前を渡す。 そのため本物は会話ログの 1 行目に custom-title を書く。

{"type":"custom-title","customTitle":"cost-management","sessionId":"…"}

アプリや VS Code から開いたセッションは名前が会話の内容から自動で決まるので、 この行が先頭に来ない(あとから付くことはある)。

実測(2026-08-23、会話ログ 247 本): 立っている ccs セッション 12/12 に印があり、 デスクトップから開いたセッションには無かった。

追記(2026-08-31、I3b): 値まで見るようにした。 以前は「1 行目に custom-title を 含むか」だけを見ていたので、ccs 以外が -n を付けて起動したセッションも通っていた (実機で 1 件あった ── 1 行目が bglab webサイト で slug と一致しないのに候補に並んでいた)。

いまは customTitle の値が ccs がそのパスに対して計算する slug と一致するかまで見る。 slug は決定的に計算できるので、一致すればほぼ断定できる。

リポジトリ名の曖昧さ解消などで slug が変わっていれば拾えなくなるが、それは名指しで戻せる。

アプリ側の実装に依存した判定なので、外れたときは「候補に出ない」側に倒れる ── 黙って知らない会話を立てるのではなく、名指しを求める形で失敗する。

列挙できないもの

ghq の外に立てたセッション(ccs new /some/path)は列挙できない。ccs は ghq 配下しか舐めないため。名指しなら戻せる。

$ ccs restore x01              # リポジトリ
$ ccs restore x01--topic      # worktree(slug は `--` 区切り)
$ ccs restore tmp-3f9a2c1b     # 作業枠(id まで要る)
$ ccs restore /some/path       # ghq の外

リポジトリ配下の worktree は ghq list に出ない

ghq は .git を見つけた時点でそれ以上降りないので、<repo>/.worktrees/<name> は 独立したリポジトリとして列挙されない(実測。ADR-0003)。 だから候補 3 は ghq list の各リポジトリに .worktrees/ があるかを見て回る。

<repo>.worktrees/<name>(リポジトリの兄弟)は出る。 手でそこに切った worktree があれば、<name> という slug で候補 4 に混ざる。

既定は見せるだけ

$ ccs restore
立て直せるセッション:
  tmp-1   2026-08-22 13:45  caf95a3f-157d-4012-860a-5c03606ba20a  ドキュメント整理
  tmp-2   2026-08-22 13:45  c56885d3-6808-4eb1-bd7b-5d39817ac670  設計メモ

実行するには: ccs restore --yes
別の会話を選ぶ: ccs restore <slug> --list

ccs gc と同じ作法で、--yes を付けるまで何も起きない。 理由は掃除とは別で、 戻した会話はその場で動き出すことがあるため ── 落ちる直前に続きがあった会話は、 resume した瞬間に続きを走らせる(実測で 1 本が busy になった)。

ccs にこれを止める手段は無い。 何をするかは会話の側が持っているので、 ツールから抑えられるのは「戻す本数を選べること」だけ。1 本ずつ戻したいときは 名指しする。

$ ccs restore tmp-3 --yes      # これだけ戻す

どの会話を戻すか

既定は 最後に触られた会話。同じ場所で会話を何度も立てていれば候補は複数あるが、 落ちる直前に動いていたのは最後の 1 つ。

ただし止まったペインは、そのペインが覚えている uuid が勝つ。ccs はペインを exec $SHELL で残すので、起動コマンドの文字列から「そのペインが何の会話だったか」を 直接引ける。同じ場所で別の会話を立てていた場合、最新の会話ログはそのペインの会話とは 限らない。

古いものに戻すときは、まず並べてから名指しする。

$ ccs restore tmp-1 --list
tmp-1  ~/.cc-scratch/1
  caf95a3f-157d-4012-860a-5c03606ba20a  2026-08-22 13:45  ドキュメント整理
  9d0e1f2a-...                          2026-08-19 09:12  週次の棚卸し

$ ccs restore tmp-1 --session-id 9d0e1f2a-... --yes

古さの線

会話ログは消えないので、上限が無いと数か月前に使い終えた枠まで候補に並ぶ。 既定では直近 7 日(CCS_RESTORE_MAX_AGE、0 で無制限)に触られた会話だけを拾う。 --all でその場だけ外せる。

効くのは列挙するときだけ。 名前で指したもの(ccs restore tmp-3)はどれだけ 古くても戻す ── 人が名前を打っているなら、それが意図。

前回の停止まで生きていた組だけ戻す(--last)

再起動の直後は、候補に 2 種類が混ざる。

  • 一緒に落ちた組 — 停止の瞬間まで生きていた。戻したいのはこちら
  • それ以前からの残骸 — 何日か前に畳んだきりのもの。戻すと人が困る

ccs restore --yes は両方を立てる。片方だけ戻すのに slug を目で選んで並べるのは、 再起動のたびにやると割に合わない(実測: 候補 19 本のうち戻したいのは 13 本)。

$ ccs restore --last
立て直せるセッション:
  tmp-1   2026-08-27 20:50  caf95a3f-…  ドキュメント整理
  x01     2026-08-27 20:50  e23e581f-…  x01
  …

実行するには: ccs restore --last --yes

引数の手打ちが要らないので、hub からそのまま叩ける。既定は ccs restore と 同じく dry-run で、実行は --last --yes。

「生きていた」をどう決めているか

会話ログの mtime は「最後に活動した時刻」ではなく「最後に生きていた時刻」。

OS のシャットダウンは生きている claude に SIGTERM を送り、claude はそこで 会話ログを書き切る。だから丸 1 日アイドルだったセッションも、停止の瞬間に 書き込まれる。

実測(2026-08-27):

見たもの 値
last reboot の shutdown time 20:50
sysctl -n kern.boottime 23:22:44
停止の瞬間に mtime が並んだ会話ログ 13 本(20:50:08 〜 20:50:53)
そのうち 2 本の最後の実内容 前日(8/26)の assistant エントリ
末尾の行 timestamp を持たないメタ行(last-prompt / bridge-session)

丸 1 日何も起きていない会話が停止時刻の mtime を持っている ── これが「停止の 瞬間に書き切っている」ことの証拠になる。

効くのは、手で畳んだセッションはその瞬間に書き込みが止まるから。 一斉に 書かれた塊に乗っているかどうかで、「一緒に落ちた」と「その前に畳んだ」を分けられる。 時刻の塊を見る根拠はこの一点にある ── 単なるクラスタリングとしてやっているのではない。

切り出しの手順

塊を自前で切るだけだと閾値の根拠が無いので、起動時刻を外の基準点として使う。

  1. 起動時刻 T_boot を OS に訊く(macOS: kern.boottime / Linux: /proc/stat の btime)
  2. 候補のうち mtime < T_boot のものだけを見る(起動後に動いたものは「前回」ではない)
  3. その最大値 T_last を取る(=実質、停止の瞬間)
  4. [T_last - CCS_RESTORE_LAST_WINDOW, T_boot) に入るものを戻す

窓の幅は既定 5 分(CCS_RESTORE_LAST_WINDOW)。停止は一瞬ではない ── シャットダウンは生きているプロセスへ順に SIGTERM を送るので、一緒に落ちた組でも 数十秒ばらける(実測で 13 本 45 秒)。広げると手で畳んだものを巻き込み、狭めると 取りこぼす。

--last は列挙の古さの線(CCS_RESTORE_MAX_AGE)を外す。 マシンが 7 日以上 落ちていたら、戻したい組ごと落とされてしまうため。外したぶんは窓が締める。

起動時刻が読めない環境では 2 を飛ばし、警告を 1 行出したうえで塊だけで判断する。 CCS_RESTORE_BOOT_EPOCH に epoch 秒を書けば明示できる。

スナップショットを持たない理由

ccs が「さっきまで何が立っていたか」を自分で書き留める案は採っていない。

  • 状態ファイルを持たず tmux から導出するのがこのツールの制約 (design.md §2.1)
  • そもそも要らない ── 上のとおり、会話ログの mtime が既にスナップショットになっている
  • 仮に持つとしても書くタイミングが詰む。 定期に書けばデーモンが要り (design.md §8で禁じている)、shutdown フックは電源断・パニック・ SIGKILL で走らない。走らない場面がまさに一番欲しい場面

塊ができない止まり方(--since)

電源断・強制再起動では SIGTERM が届かないので、上の一斉書き込みが起きない。 そのとき --last は「単に直近に触ったもの」に劣化する。

その逃げ道が --since <期間>。現在時刻から遡る幅を人が決める。

$ ccs restore --since 6h
$ ccs restore --since 6h --yes

受け付ける綴りは <数><s|m|h|d> だけ。絶対時刻は受け付けない ── epoch へ 直す綴りが BSD(date -j -f)と GNU(date -d)で割れていて、書き分けると 「どちらの環境でも試していない分岐」が必ず残る。

--last は「起動時刻を基準にした --since」なので、絞り込みの仕組みは共通。 どちらも slug の名指しとは一緒に使えない(名指しは絞り込みではなく答えそのもの)。

素の ccs restore は「--last なら何本か」を併記する

ccs restore --yes は 7 日以内に触った会話を全部立てる。 再起動のあとに欲しいのは たいてい「前回の停止まで生きていた組」だけなので、そこへ案内する。

$ ccs restore
戻せます:
  x01              2026-08-30 19:19
  tokyo-dungeon    2026-08-25 19:06
  …

実行するには: ccs restore --yes
  前回の停止まで生きていた組だけなら 2 本: ccs restore --last --yes

既定は変えていない。 出すのは案内だけ ── 既定を --last にすると、--all を知らない人の 手元で挙動が変わる(#89 の回答は「まず併記、様子を見て既定を変える」)。

出さない場面が 3 つある。

理由
0 本 「--last なら 0 本です」は情報が無い
全部と同じ本数 絞る意味が無い
既に --last / --since を付けている 同じことを二度言わない

数えるのと絞るのは同じ関数を通る(restore_rows_last)。別実装にすると、いつか食い違って 嘘の本数を出す。

戻せない理由

4 つの関門を全部通ったものだけが戻る。どれも「黙って間違ったものを立てる」より 「立てない」を選ぶ形に倒してある。

関門 落ちると なぜ要るか
その会話が生きていないか (一覧に出ない) 戻すと同じ会話に 2 本目が立つ
会話ログがあるか 会話ログがありません 戻すものが無い
作業ディレクトリがあるか 作業ディレクトリがありません 作業枠だけは例外で作り直す(ccs が作った空のディレクトリなので失うものが無い)
枠が空になっていないか 会話が別の場所へ移り、枠は空です 昇格した会話を戻さない(#94)
記録された cwd に、その場所があるか 会話ログの cwd が違います エンコード規則の答え合わせ

cwd は 1 本の会話でも変わる

「最後の cwd が一致するか」で見てはいけない。 会話ログの cwd は 1 行ごとに記録され、 同じ会話でも値が変わる。実測(2026-09-07)で最後に来ていたもの:

最後の cwd 何が起きたか
~/.claude/uploads/<uuid> スマホから画像を上げた
~/.claude/skills スキルを実行した
/private/tmp/…/scratchpad 一時ファイルを作った(/tmp は /private/tmp への symlink)

一致で見ると、よそを触っただけの会話まで落ちる(実際に 4 本落ちた)。 だから昇格の判定は 枠が空かどうかを直接見て、エンコードの答え合わせは 記録されたどれか 1 つに一致すればよい、という形に分けてある。

選んで戻す(--pick)

終了判定は context ベース ── 人が見ないと決まらない。

--last は「一緒に落ちた組」を会話ログの最終更新時刻の塊でしか切れないので、 「会話は終わっていないが 1 日放置している」を取りこぼし、直前に意図して畳んだものを 巻き込む。判定を自動化するのをやめて、人に選ばせる口が --pick。

$ ccs restore --pick
戻すものを選んでください:

   1) x01           2026-08-30 19:19  x01 の詰まり調査
                    ROADMAP の I1 を片付けて
   2) tokyo-dungeon 2026-08-25 19:06  tokyo-dungeon

番号(例: 1 3 / 1-3 / all / none / Enter で中止): 1
戻しました: cc/x01(e6b1a81d-…)
  • 判断材料を並べる。 slug と最終更新だけでは選べないので、会話のタイトルと 直近の依頼(ccs ls -l の REQUEST と同じもの)まで出す
  • 選ぶこと自体が承認。 --yes は要らず、併記もできない
  • --last / --since / 名指しと併用できる。 絞ってから選ぶ形
  • 重ねて打っても 2 度立てない(1-3 1 のような指定でも 1 本ずつ)
  • 中止は失敗ではない。 Enter だけなら何も戻さず 0 で終わる

非対話では固まらない。 パイプ越しや launchd から呼ばれたときは、attach の番号選択と 同じ形で断る(--yes か --json を使う)。--json との併用もできない ── 対話で選ぶ経路と 機械可読な経路は別。

選ばなかったものは、次から出ない

一度「終わった」と判断した会話が毎回並び続けると、選ぶ手間が減らない。 そこで --pick は、選ばれなかったものを「人がこれは死んだと決めた」として覚える。

$ ccs restore --pick
   1) x01           …
   2) tokyo-dungeon …

番号(例: 1 3 / 1-3 / all / none / Enter で中止): 1
戻しました: cc/x01(e6b1a81d-…)

$ ccs restore            # tokyo-dungeon はもう並ばない
戻せるものはありません。
  • Enter は逃げ道のまま。 見に来ただけで反射で Enter を打つのは普通なので、そこに 「全部死んだ」を割り当てない。全部いらないときは none と明示する
  • 名指しには効かない。 ccs restore <slug> は「これを戻す」という答えそのものなので、 記録があっても戻せる(--json の列挙にも同じ記録が効く)
  • 戻したら記録は消える。 生き返ったものを死んだと覚えたままにすると、次に死んだとき 候補に出ない
  • 会話ログが消えた行は、書き込みのたびに落とす。 放っておくと増え続けるため
  • ccs kill で畳んだ会話も同じ記録に入る(#89 の 3)。 ccs kill --self も、ccs gc --yes がアプリで閉じられたものを 畳んだときも同じ。人が畳んだものは、再起動のあとに生き返らない。 kill → ccs restore <slug> で作り直す経路は、名指しなので通る

記録は CCS_DISMISSED_FILE(既定 ~/.config/ccs/dismissed)に <sessionId><TAB><cwd> の 1 行 1 件で置く。消せば全部やり直せる(rm してよい)。

[!NOTE] これは「セッションの索引」ではない。 ADR-0002 決定 6 は ccs が自前のレジストリを 持たないと決めているが、あれが禁じたのは存在を二重に持つこと(ディレクトリと索引が ずれる)。ここが持つのは他のどこにも無い人の判断で、会話ログにもレジストリにも印にも 書かれていない ── ずれる先が無い。

機械で読む(--json)

ハブのエージェントが結果を読めるように、--json を付けると 1 つのオブジェクトを返す。

$ ccs restore --json
{"applied":false,
 "ready":[{"slug":"tmp-1","path":"…","sessionId":"…","tmux":"cc/tmp-1",
           "updatedAt":1788055863,"title":"…"}],
 "alive":[],
 "skipped":[]}

$ ccs restore --json --yes
{"applied":true,
 "restored":[{"slug":"tmp-1","sessionId":"…","tmux":"cc/tmp-1"}],
 "failed":[],
 "alive":[],"skipped":[]}

applied で分岐すれば足りる。 人間向けの 3 つの見出し(立て直せる / 生きているので 触りません / 戻せません)を読み分けさせない、というのがこの形の理由。

  • 戻すものが無くてもキーの形は変わらない。 空のときだけ別の形にすると、読む側が 2 通りの分岐を持つことになる
  • --yes を付けたら必ず 1 つ返す。 黙って終わると「失敗したのか、対象が無かったのか」 を区別できない
  • stdout は JSON だけ。 人間向けの案内は stderr へ出す(混ぜるとパースできなくなる)
  • --list --json は、その場所に残っている会話を conversations に並べる

戻さないもの

相手 理由
生きているセッション 触らない(冪等)。二重に立てると同じ作業ツリーを 2 本の claude が触る
hub ccs hub up(自動起動)が立て直す担当。同じ会話に戻すなら ccs hub restart --resume
会話ログが無いもの 戻す先が無い。理由を出して飛ばす
作業ディレクトリが消えたもの 同上。作業枠だけは例外で、無ければ作り直す(ccs が作る空のディレクトリなので失うものが無い)
会話ログの cwd が食い違うもの 次節
アプリで閉じられたもの 人の判断そのもの。名指しか --all なら戻す(次々節)
ccs kill で畳んだもの 同上。--pick の記録と同じ扱い。名指しなら戻す

アプリで閉じられた会話

claude.ai やスマホアプリで「アーカイブ」「削除」すると、ローカルの claude は Remote Control を切り、会話ログに system 行を 1 本書く(文面は board.md)。プロセスはそのまま生きるので、 再起動で落ちるまでは ccs gc が畳む相手になる。

落ちたあとに ccs restore が拾うと、人が畳んだものが生き返る。だから列挙では 戻さず、「戻せません」に理由つきで出す。

$ ccs restore
戻せません:
  tmp-ba825913  アプリで閉じられています(archived)。戻すなら名指しで: ccs restore <slug>
  • 名指し(ccs restore <slug>)なら戻す。 名前を打っているなら、それが意図
  • --all なら候補に入れる。 古いものも閉じたものも全部、の指定
  • disconnect の行より後に人が打った依頼があれば、閉じていないと読んで普通に戻す。 ccs attach で続けた会話がそれ。hub からのメッセージは数えない

ccs gc --yes で畳んだときは、あわせて「終わった」の記録(--pick の記録と 同じもの)にも入れる。会話ログを読み直すまでもなく候補から外れる。

エンコード規則への依存

会話ログの置き場所は「cwd の英数字以外をすべて - に潰したもの」(実測)。

/Users/you/.cc-scratch/1  →  -Users-you--cc-scratch-1

この規則は逆算できない。 / も . も _ も同じ - に潰れるので、 -cc-hub が .cc-hub なのか cc-hub なのかはディレクトリ名からは決められない。

claude --resume の一覧に頼る手も無い。値なしの --resume は対話ピッカーを開くだけで、 機械可読な一覧を出すサブコマンドは無い(claude --help を確認)。

そこで ccs は 規則を前向きにだけ使い、当たった会話ログの中の cwd で答え合わせをする。 食い違ったら、黙って別の会話を戻す代わりにその 1 本を飛ばす。

$ ccs restore
戻せません:
  tmp-1   会話ログの cwd が違います(~/somewhere/else)

規則が変わったらここで気づける、というのがこの作りの目的。

会話の名前は上書きしない

ccs new は claude -n <slug> で名前を渡すが、ccs restore は -n を渡さない。

戻す会話には既に名前が付いていて、アプリの一覧に出ているのはその名前 (人が付けた名前がそのまま残る)。ここで slug を被せると、戻した瞬間に tmp-2 へ 改名され、探している名前のほうが消える。

hub up との関係

ccs hub up は restore をしない。

自動起動から 5 分おきに走るので、ここで他のセッションまで戻すと、人が畳んだものが 5 分後に生き返る。ccs は状態を持たないので、durable な情報からは「畳んだ」と 「落ちた」を区別できない ── 区別できるのは打った本人だけなので、復元は人が打つ ccs restore に残してある。

代わりに、hub を実際に立て直したときだけ(=生きていなかったとき。再起動の直後が まさにこれ)、他に止まっているものがある旨を 1 行出す。

$ ccs hub up
{"slug":"hub","state":"healthy",...}
ccs: 止まっているセッションが 19 本あります(hub は立て直しました)。
  前回の停止まで生きていた組だけ:
    見る:     ccs restore --last
    立て直す: ccs restore --last --yes
  止まっているもの全部(19 本): ccs restore

--last を先に出している。 ここへ来るのはたいてい再起動の直後で、候補には 残骸が混ざる。素の ccs restore --yes を既定の導線にすると、そこまで立ち上がる。

終了コード

値 意味
0 成功(見せただけのときも 0)
1 名指しした対象に会話が無い / 戻したセッションが時間内に登録されなかった
2 使い方の誤り