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 --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 を書く。
アプリや 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 本ずつ戻したいときは
名指しする。
どの会話を戻すか¶
既定は 最後に触られた会話。同じ場所で会話を何度も立てていれば候補は複数あるが、 落ちる直前に動いていたのは最後の 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 を持っている ── これが「停止の 瞬間に書き切っている」ことの証拠になる。
効くのは、手で畳んだセッションはその瞬間に書き込みが止まるから。 一斉に 書かれた塊に乗っているかどうかで、「一緒に落ちた」と「その前に畳んだ」を分けられる。 時刻の塊を見る根拠はこの一点にある ── 単なるクラスタリングとしてやっているのではない。
切り出しの手順¶
塊を自前で切るだけだと閾値の根拠が無いので、起動時刻を外の基準点として使う。
- 起動時刻
T_bootを OS に訊く(macOS:kern.boottime/ Linux:/proc/statのbtime) - 候補のうち
mtime < T_bootのものだけを見る(起動後に動いたものは「前回」ではない) - その最大値
T_lastを取る(=実質、停止の瞬間) [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 <期間>。現在時刻から遡る幅を人が決める。
受け付ける綴りは <数><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 <slug>)なら戻す。 名前を打っているなら、それが意図 --allなら候補に入れる。 古いものも閉じたものも全部、の指定- disconnect の行より後に人が打った依頼があれば、閉じていないと読んで普通に戻す。
ccs attachで続けた会話がそれ。hub からのメッセージは数えない
ccs gc --yes で畳んだときは、あわせて「終わった」の記録(--pick の記録と
同じもの)にも入れる。会話ログを読み直すまでもなく候補から外れる。
エンコード規則への依存¶
会話ログの置き場所は「cwd の英数字以外をすべて - に潰したもの」(実測)。
この規則は逆算できない。 / も . も _ も同じ - に潰れるので、
-cc-hub が .cc-hub なのか cc-hub なのかはディレクトリ名からは決められない。
claude --resume の一覧に頼る手も無い。値なしの --resume は対話ピッカーを開くだけで、
機械可読な一覧を出すサブコマンドは無い(claude --help を確認)。
そこで ccs は 規則を前向きにだけ使い、当たった会話ログの中の cwd で答え合わせをする。
食い違ったら、黙って別の会話を戻す代わりにその 1 本を飛ばす。
規則が変わったらここで気づける、というのがこの作りの目的。
会話の名前は上書きしない¶
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 | 使い方の誤り |