Repository navigation
スナップショットが作り直せるキャッシュ・一時ファイル・ログまで控える(除外の仕組みが無い) #368
Description
何を見つけたか
スナップショット(devbase snapshot)は、対象のボリューム(共通 devbase_home_ubuntu とグループのボリューム)を丸ごと控え、除外の仕組みを持たない(lib/devbase/snapshot/manager.py の backup_command は tar --listed-incremental=... -cf - -C /source <各マウント先> だけで、除外の指定が無い)。作り直せるキャッシュ・一時ファイル・ログも毎回控えている。
手元のボリュームで測った内訳のうち、控えなくてよいもの:
| パス(ボリューム内) | 大きさ | 中身 | 外してよい理由 |
|---|---|---|---|
.gemini/tmp/<プロジェクト>/ の chats/ 以外(logs.json・logs/ など。共通・グループの両方) |
共通側で約 325MB(2026-10-02) | Gemini の作業用のログ・一時ファイル | 一時ファイル・ログ。ただし chats/ 以外に復元で要るものが無いかは未確認 |
.codex/logs_2.sqlite(共通) |
602MB | Codex の動作ログ(logs テーブルだけ。直近約 9.5 日分で古い行から消える) |
会話の履歴は sessions/・thread_history_1.sqlite・state_5.sqlite にある。使う間ずっと書き換わるため、差分のたびにファイル全体が入る |
.codex/.tmp・.codex/cache(共通) |
約 200MB | 一時ファイル・キャッシュ | 作り直せる |
.claude/plugins/cache・.claude/plugins/marketplaces(共通) |
約 607MB・約 70MB | プラグインとマーケットプレイスの取得物 | 配布元から取り直せる |
.serena/logs(共通) |
158MB | 日付ごとのログ(消されず溜まる) | 復元で要らない |
外さないもの:
- 会話の記録(
.claude/projects・.codex/sessions・thread_history_1.sqlite・state_5.sqlite・.gemini/tmp/<プロジェクト>/chats/)。.gemini/tmpは名前に反して Gemini の会話の記録(chats/session-*.jsonl)を持つ。共通側の.gemini/tmp約 1.3GB のうち約 984MB がchats/である(2026-10-02)。.gemini/tmpを丸ごと外すと会話の記録を失う .claude/pluginsの状態のファイル(installed_plugins.json・known_marketplaces.json・data/)。入れたプラグインとマーケットプレイスの一覧と、プラグインが持つデータで、取り直せない.serena/language_servers— 取り直しは TLS を傍受するプロキシの下で失敗しやすく、手で差し替えた版(*.bak-*)がある。変わらないため差分への負担は小さい
決めること
- 除外の一覧を持つ場所(既定の一覧を本体に持つか、利用者が足せるか)
- 除外の単位。
.gemini/tmpと.claude/pluginsは、ディレクトリ丸ごとではなく中の一部だけを外す必要がある - 復元したときに除外したパスが無い状態で、各 CLI が起動できること(
logs_2.sqliteは_sqlx_migrationsを持つので作り直すと見ているが未確認。.claude/plugins/cacheが無いときにプラグインを取り直すか) - 世代の途中で除外を変えたときの扱い。差分は GNU tar の
--listed-incrementalで作っており、前の世代に入っていたパスを次の差分で除外すると、そのパスは消えたものとして記録される。差分を順に展開する復元(--listed-incremental=/dev/null)では、そのパスが消える。除外を足すときは新しいフルから始めるなどの扱いが要る
関連
差分バックアップが毎回全体を控えていた不具合(tar の起点の修正、PR #369 でマージ済み)とは別。そちらの修正で変わらないファイルは差分に入らないため、この課題の効果は主に「頻繁に書き換わるもの」とフルバックアップの大きさに出る。
由来
利用者からの依頼(2026-09-30、差分バックアップが遅い件の調査)
依頼(原文)
利用者からの依頼(2026-09-30、差分バックアップが遅い件の調査)
(書き手: 起票者(課題の本文の「由来」の節)、日付: 2026-09-30。依頼の原文の文は課題に残っていない。下の「何を見つけたか」以降の表と条件は、起票者が調査で書いたものである)
除外の一覧は 本体に既定の一覧を持ち、利用者が設定で足せる 形にする。既定の一覧は本文の「控えなくてよいもの」の表から作る(
.gemini/tmp/<プロジェクト>/chats/は外さない)。残りの「決めること」(除外の単位、除外したパスが無いときの各 CLI の起動、世代の途中で除外を変えたときの扱い)は設計の中で決める。これで判断待ちは解けた。
(書き手: 利用者(課題のコメント「決定(2026-10-11 利用者)」)、日付: 2026-10-11)
#368。人へ問わずに進め、決められない点は前提か未決として課題の本文へ書く。
(書き手: AI の起動指示、日付: 2026-10-12。利用者の指示の原文として扱わない)
起票者の推定
題・本文のうち、利用者の指示の原文に無く起票者が推して足した条件。承認ゲート 1 で利用者が確かめるまで、利用者の決定として扱わない。
| # | 条件 | 推した出所 | この要求での扱い |
|---|---|---|---|
| S1 | .gemini/tmp/<プロジェクト>/ のうち chats/ 以外(logs.json・logs/ など)を外す。共通・グループの両方 |
課題の本文の「控えなくてよいもの」の表(2026-10-02 の手元の測定)。chats/ 以外に復元で要るものが無いかは未確認と本文が書いている |
受け入れ条件 1・2。未確認の点は前提 3 |
| S2 | .codex/logs_2.sqlite(共通)を外す |
同じ表。会話の履歴は sessions/・thread_history_1.sqlite・state_5.sqlite にあると起票者が調べた |
受け入れ条件 1 |
| S3 | .codex/.tmp・.codex/cache(共通)を外す |
同じ表 | 受け入れ条件 1 |
| S4 | .claude/plugins/cache・.claude/plugins/marketplaces(共通)を外す |
同じ表。配布元から取り直せると起票者が推した | 受け入れ条件 1。取り直すかは前提 4 と受け入れ条件 9 |
| S5 | .serena/logs(共通)を外す |
同じ表 | 受け入れ条件 1 |
| S6 | 会話の記録(.claude/projects・.codex/sessions・thread_history_1.sqlite・state_5.sqlite・.gemini/tmp/<プロジェクト>/chats/)は外さない |
本文の「外さないもの」 | 受け入れ条件 2 |
| S7 | .claude/plugins の状態のファイル(installed_plugins.json・known_marketplaces.json・data/)は外さない |
本文の「外さないもの」 | 受け入れ条件 2 |
| S8 | .serena/language_servers は外さない |
本文の「外さないもの」。TLS を傍受するプロキシの下で取り直しが失敗しやすく、手で差し替えた版がある | 受け入れ条件 2 |
| S9 | 世代の途中で除外を変えると、差分を順に展開する復元でそのパスが消える。新しいフルから始めるなどの扱いが要る | 本文の「決めること」の 4 つ目(GNU tar の --listed-incremental の振る舞いからの推定) |
受け入れ条件 6・7。扱いの形は設計で決める |
| S10 | logs_2.sqlite は _sqlx_migrations を持つので、無ければ作り直す |
本文の「決めること」の 3 つ目(未確認と本文が書いている) | 前提 4 と受け入れ条件 9 で確かめる |
目的
- スナップショットが、作り直せるもの(一時ファイル・ログ・取り直せる取得物)を控えずに済むようにし、フルの世代と頻繁に書き換わるファイルの差分の大きさを減らす
- 会話の記録・入れたプラグインの一覧・利用者が手で整えたものは、これまでどおり控え、復元で戻る
前提
- 前提 1: 対象は devbase のスナップショット(
devbase snapshot create・devbase upの自動スナップショット・restoreの前に作る復元前バックアップ)が作るアーカイブである。snapshot copyは既存の世代のアーカイブを写すだけで、除外を新たに適用しない - 前提 2: 既定の除外の一覧は、課題の本文の「控えなくてよいもの」の表(S1〜S5)から作る。利用者の決定(2026-10-11)に従う。表のパスが端末に無いときは、そのパスを除外しても何も起きない
- 前提 3:
.gemini/tmp/<プロジェクト>/のchats/以外に、Gemini の復元で要るものは無い。誤っていたら、そのパスを既定の除外の一覧から外す形で直せる(局所的に直せる) - 前提 4: 除外したパスが無い状態から、Claude Code・Codex・Gemini CLI・Serena は起動でき、要るものを作り直すか取り直す。成否はマージの前に受け入れ条件 9 で確かめ、起動できない CLI があれば、そのパスを既定の除外の一覧から外す
- 前提 5: 利用者が足す除外は、ボリュームの中の相対のパス(とその下)で書く。書き方(パターンの記法・置き場)は設計で決める
- 前提 6: 除外の一覧が変わったことで、更新後の最初の自動スナップショットが系列ごとに新しいフルの世代を作ってよい(1 系列につき 1 回のフルの取り直し)。既存の世代のアーカイブは書き換えない
対象範囲
含む:
- 既定の除外の一覧を本体に持ち、作成(フル・差分)と復元前バックアップのアーカイブへ適用すること
- 利用者が設定で除外を足せること
- 除外の一覧が前の控えと違うときに、復元で除外の外のデータを失わない扱い
- 除外したパスが無い状態で復元した後に、各 CLI が起動できることの確認
- 利用者向けの文書(スナップショットガイド・CLI リファレンス・設定の文書)と
CHANGELOG.mdの[Unreleased]
含まない:
- 既定の除外の一覧から利用者が項目を外すこと(利用者の決定は「足せる」だけである)
- 既にある世代のアーカイブから除外のパスを取り除くこと(作り直さない)
- 復元の対象・順序・失敗時の案内の変更(除外したパスが復元後に無いことを除く)
- 世代の保持(ローテーション)・系列・最小間隔の規則の変更
- 各 CLI のログやキャッシュの置き場・保存の期間を変えること(CLI 側の設定)
snapshot copyで除外を適用し直すこと- base イメージに入れる道具の変更(P1 に当たる変更はしない)
ドメインイベント
| # | イベント | 引き金 | 失敗したとき | 順序の前提 |
|---|---|---|---|---|
| E1 | 除外の一覧を決めた(既定の一覧と利用者が足した分を合わせた) | create・自動スナップショット・復元前バックアップの開始 |
利用者の設定が読めない・不正な項目がある → 受け入れ条件 11・12 | — |
| E2 | 積み先の世代の除外の一覧と、今の除外の一覧を比べた | 自動スナップショットの積み先の判定・名前を指定した差分 | 前の世代が除外の一覧を記録していない(この変更より前の世代) → 違うものとして扱う(受け入れ条件 6) | E1 |
| E3 | 新しいフルの世代を作った(除外を適用した) | E2 で違う・系列に世代が無い・差分の上限 | tar が失敗 → 既存の create の失敗の扱い(変えない) |
E1・E2 |
| E4 | 差分を積んだ(同じ除外を適用した) | E2 で同じ | 同上 | E1・E2 |
| E5 | 世代に、作ったときの除外の一覧を記録した | E3・E4 | 書けない → 既存の meta.yml の書き込みの失敗の扱い |
E3 か E4 |
| E6 | 世代を復元した(ボリュームを空にしてから展開した) | devbase snapshot restore |
既存の扱い(変えない) | E5 |
| E7 | 復元後に CLI を起動し、除外したパスが無い状態から CLI が要るものを作り直した | 利用者の CLI の起動 | 起動できない → 前提 4 に従い既定の除外から外す | E6 |
E2 は E1 の順序を仮定しているため、受け入れ条件 6・7 へ移した。E7 は引き金が devbase の外(利用者の CLI の起動)にあるため、前提 4 と受け入れ条件 9 へ移した。
用語
| 用語 | 意味 |
|---|---|
| 除外の一覧 | スナップショットのアーカイブに入れないパスの一覧。既定の除外と利用者の除外を合わせたもの |
| 既定の除外 | devbase の本体が持つ除外の一覧。作り直せる一時ファイル・ログ・取り直せる取得物を挙げる |
| 利用者の除外 | 利用者が設定で既定の除外に足すパス |
受け入れ条件
- 1. 前提: 共通ボリュームとグループのボリュームに、既定の除外のパス(
.gemini/tmp/<プロジェクト>/logs.json・.gemini/tmp/<プロジェクト>/logs/・.codex/logs_2.sqlite・.codex/.tmp・.codex/cache・.claude/plugins/cache・.claude/plugins/marketplaces・.serena/logs)がある。操作:devbase snapshot create --fullで世代を作る。結果:full.tar.zstの中身の一覧(tar -t)に、どのパスもその下のファイルも無い - 2. 同じ世代の
full.tar.zstの中身の一覧に、.claude/projects・.codex/sessions・.codex/thread_history_1.sqlite・.codex/state_5.sqlite・.gemini/tmp/<プロジェクト>/chats/の下のファイル・.claude/plugins/installed_plugins.json・.claude/plugins/known_marketplaces.json・.claude/plugins/data/・.serena/language_serversがある(置いたパスのとき) - 3. 差分(
incr-NNN.tar.zst)でも同じ除外が効き、前の控えから書き換えた.codex/logs_2.sqliteが差分の中身の一覧に無い - 4. 復元前バックアップ(
pre-restore-*)のアーカイブにも同じ除外が効く - 5. 利用者が設定で除外を 1 つ足すと、次に作る世代のアーカイブの中身の一覧にそのパスとその下が無く、既定の除外も引き続き効いている
- 6. 前提: 系列の最新の世代が、今と違う除外の一覧で作られている(この変更より前に作った、除外を持たない世代を含む)。操作:
devbase upの自動スナップショット。結果: その世代へ差分を積まず、系列に新しい世代を作り、その理由をログの 1 行で示す - 7. 前提: 名前を指定して、今と違う除外の一覧で作った世代へ差分を作ろうとする。結果: 差分を作らず、理由を示して止まる(終了コード 1)。その世代のアーカイブと
snapshot.snarは変わらない - 8. 除外を適用した世代を復元すると、除外の外のパスは控えた時点の中身に戻り、除外したパスは復元後のボリュームに無い(復元の前にボリュームを空にするため)
- 9. 前提: 既定の除外のパスを消したホームで dev コンテナを起動する。操作: Claude Code・Codex・Gemini CLI・Serena をそれぞれ起動する。結果: どれもエラーで止まらずに起動し、Claude Code は入れたプラグインの一覧(
installed_plugins.json)のプラグインを使える状態になる - 10. この変更より前に作った世代(除外を持たない世代)を、変更後の devbase で一覧・復元でき、復元の結果は変更前の devbase で復元したときと同じである
- 11. 利用者の除外に、ボリュームの外を指す値(絶対パス・
..の要素を含む)や空の値を書くと、その値を採らずに警告を 1 行出し、残りの除外の一覧で控えを作る - 12. 利用者の除外を書いていないときは、既定の除外だけで控えを作り、警告を出さない
- 13. 除外のパスが端末に無いとき(その CLI を使っていない)、控えの作成は失敗せず、警告も出さない
- 14. 世代ごとに、作ったときの除外の一覧が後から読める(世代のメタ情報に残る)
非機能の条件
| 大項目 | 条件 |
|---|---|
| 性能・拡張性 | 課題の本文の測定(2026-10-02)と同じ構成で、フルの世代の展開前の控えの対象から、既定の除外のパスの合計(約 1.9GB)が減る。測った端末とボリュームの大きさを記録に残す |
| 運用・保守性 | 除外の一覧が前の控えと違うために新しい世代を作るときは、理由をログの 1 行で示す(受け入れ条件 6)。世代ごとの除外の一覧が後から読める(受け入れ条件 14) |
| 移行性 | 既存の世代は書き換えずにそのまま一覧・復元できる(受け入れ条件 10)。更新後の最初の自動スナップショットで、系列ごとに新しいフルの世代を作る(前提 6)。切り戻し(変更前の devbase へ戻す)で、変更後に作った世代を復元すると除外したパスは無い |
影響
| 対象 | 影響 |
|---|---|
| 公開インタフェース | 変わる。利用者の除外を足す設定が増える(互換を壊さない追加)。CLI の引数は変えない |
| データ | 世代のメタ情報に除外の一覧が増える。既存の世代は書き換えない。スナップショットに入らなくなるパスがあり、復元後にそのパスは無い |
| 既存の振る舞い | 控えの中身から既定の除外のパスが消える。更新後の最初の自動スナップショットが系列ごとに新しい世代を作る。違う除外の一覧の世代へ名前を指定して差分を作ると止まる |
検証手段
| 項目 | 手段 |
|---|---|
| テスト | env -u DEVBASE_ROOT uv run --locked pytest tests/ -q |
| 静的解析 | uvx ruff check --select=E9,F63,F7,F82 lib・python3 .github/scripts/proper_term_check.py |
| 手動確認(マージ前) | 受け入れ条件 1〜4・8: 作業用の名前のボリューム(利用者のボリュームではない)に既定の除外のパスと外さないパスを置き、実の devbase-snapshot イメージで作成・差分・復元を行い、アーカイブの中身の一覧と復元後のボリュームを見る。受け入れ条件 9: 使い捨てのホームのボリュームから既定の除外のパスを消して dev コンテナを起動し、4 つの CLI を起動する |
| 手動確認 | 非機能の条件の性能: 利用者の端末でリリース後に devbase snapshot create --full の大きさを変更前の世代と比べる(リリース後テスト) |
前提とする取り決め
| 項目 | 参照先 / 決めたこと |
|---|---|
| プロジェクト構造 | AGENTS.md の「このリポジトリ」。除外の規則は lib/devbase/snapshot/ に置く(確定仕様 docs/specifications/snapshot-series.md の「世代の規則は manager.py に置く」) |
| コーディング規約 | AGENTS.md の「書き方」と「検査」 |
| テスト戦略 | 単体テストは DEVBASE_ROOT を tmp_path へ向け、_run_docker_tar を差し替えて Docker を起動しない(確定仕様の「テスト観点」)。tar に渡す除外の形と積み先の判定はここで担保する。tar の除外の実際の効き目と復元の結果はマージ前の手動確認で担保する |
境界
| 区分 | 内容 |
|---|---|
| 常に行う | 既存のスナップショットのテストを通す。利用者に見える変更として CHANGELOG.md の [Unreleased] を書く。実機の記録は中立の呼び名で書く |
| 確認してから行う | 既定の除外の一覧に、課題の本文の表に無いパスを足すこと(承認ゲートで人が見る)。利用者の設定の置き場を新しく作ること |
| 行わない | 利用者のボリュームでの作成・復元の確認。既存の世代のアーカイブの書き換え。base イメージの道具の変更 |
未決
| 項目 | 誰が決めるか | 期限 |
|---|---|---|
除外の単位と書き方(.gemini/tmp/<プロジェクト>/ のうち chats/ だけを残す表し方、パターンの記法) |
設計(利用者の決定 2026-10-11 で設計に任された) | 設計の承認ゲート |
利用者の除外の置き場(devbase env の変数か、設定ファイルか) |
設計 | 設計の承認ゲート |
| 除外の一覧が違うと判定する方法(世代のメタ情報の何と比べるか) | 設計(利用者の決定 2026-10-11 で設計に任された) | 設計の承認ゲート |
| 前提 3・4 が外れたときに既定の除外から外すパス | 実装の担当(受け入れ条件 9 の結果による) | 実装の PR |
進行
モード: —
- 要求と受け入れ条件 — 2026-10-12 17:03
- 作業場所の用意 — 2026-10-12 17:40
- 設計 — 2026-10-12 17:06
- 素材の収集と出典の確定
- ドキュメント再構成
- ドキュメントレビュー
- 計画 — 2026-10-12 17:40
- 実装 — 2026-10-12 17:59
- 構造改善 — 2026-10-12 18:17
- 実装レビュー — 2026-10-12 20:11
- 完了判定 — 2026-10-12 18:09
- Pull Request — 2026-10-12 18:12
- 確定仕様化 — 2026-10-12 20:58
- 後片付け
- 配布 — 2026-10-12 21:06
- 体裁レビュー
- リリース後テスト — 2026-10-12 21:18
- 振り返り — 2026-10-12 21:19
- 棚卸し — 2026-10-12 21:27
振り返り: #462 (comment)
Activity
takemi-ohama commentedon Oct 11, 2026
決定(2026-10-11 利用者)
除外の一覧は 本体に既定の一覧を持ち、利用者が設定で足せる 形にする。既定の一覧は本文の「控えなくてよいもの」の表から作る(
.gemini/tmp/<プロジェクト>/chats/は外さない)。残りの「決めること」(除外の単位、除外したパスが無いときの各 CLI の起動、世代の途中で除外を変えたときの扱い)は設計の中で決める。これで判断待ちは解けた。
Docs: #368 の設計で、読めない除外の設定のときは今の世代の除外のまま差分を積み、古い世代を消さない(I9)
Merge pull request #459 from devbasex/design/issue-368
Merge pull request #461 from devbasex/feat/issue-368-m8a