この記事は playpark Blog からの転載です。
この記事で分かること
- auto-compact(コンテキストウィンドウ圧縮)でなぜ文脈が消えるのか、モデルの自己要約に頼る設計の限界
- 対策をモデル層・アプリ層・ハーネス層のどこに置くか、3つの選択肢の比較
- PreCompact/SessionStart hookを「書くだけ」「読むだけ」に分離する理由
背景: こういう課題があった
Claude Codeに長時間の作業を任せていると、会話がauto-compact(コンテキストウィンドウ圧縮)で要約に置き換わることがある。会話履歴が上限に近づくと発火する仕組みで、セッションを続けるためには必要な機能だが、要約はあくまで要約でしかない。何を残して何を捨てるかはそのときのモデルの重要度判断に委ねられていて、しかも漏れた情報は「漏れた」という自覚ごと消える。結果として、直前に合意した設計判断や「このテストは触らない」という制約が欠けたまま、AIは自信を持って作業を続けてしまう。
コンテキストウィンドウが大きくなればこの問題は消えるかというと、そうでもない。ウィンドウが広がるほど1セッションが長くなり、いざ圧縮が走ったときに失うものはむしろ増える。つまり圧縮は「起きるかもしれない事故」ではなく「いつか必ず来る定期イベント」として設計に織り込むほうが筋がいい。
選択肢の検討
この課題への対策は、どの層に置くかで性質が変わる。
| アプローチ | 層 | 得意なこと | 限界 |
|---|---|---|---|
| モデルの自己要約(既定の動作) | モデル層 | 設定不要で会話の流れを保つ | 何が残るかは非決定的。漏れに気づけない |
| ワークフロー側の状態ファイル | アプリ層 | 特定ワークフローの正確な再開 | 状態設計したワークフローにしか効かない |
| PreCompact/SessionStart hook(採用) | ハーネス層 | 全セッションに自動で効く | 環境から取れる汎用情報のみ運べる |
なぜこのアプローチを選んだか
モデル層に任せる既定の動作は設定不要な一方、非決定的で漏れに気づけないのが致命的だ。アプリ層の状態ファイルは正確だが、状態管理を組み込んだワークフローでしか効かず、汎用対策にはならない。
そこで採用したのがハーネス層、つまりClaude Code自体のhooksで対応する方法だ。Claude Codeのhooksには、圧縮の直前に発火するPreCompactと、セッション開始時(新規起動・再開・圧縮直後のいずれも)に発火するSessionStartがある。この2つを「PreCompactは書くだけ」「SessionStartは読むだけ」と役割を一方向に分けることで、復元経路を1本に統一できる。hooksの仕様上、stdoutがそのままcontextへ追加されるのはSessionStartを含む一部のイベントだけで、PreCompactのstdoutはデバッグログに残るのみだからだ。
実装例
退避側(PreCompact)は、git の現在地など決定的に取れる情報をファイルへ書き出す。
#!/usr/bin/env bash
set -euo pipefail
INPUT=""
[ ! -t 0 ] && INPUT=$(cat || true)
CWD=$(printf '%s' "$INPUT" | jq -r '.cwd // empty' 2>/dev/null || true)
cd "${CWD:-$PWD}" 2>/dev/null || true
PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")
DUMP_DIR="$PROJECT_ROOT/docs/session-dumps"
mkdir -p "$DUMP_DIR"
DUMP_FILE="$DUMP_DIR/session-$(date +%Y%m%d-%H%M%S).md"
{
echo "# Session Dump (pre-compact)"
echo "- branch: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo none)"
echo "## Git Status"
git status --short 2>/dev/null || true
} > "$DUMP_FILE"
find "$DUMP_DIR" -maxdepth 1 -name 'session-*.md' -type f -mtime +14 -delete 2>/dev/null || true
exit 0
読み込み側(SessionStart)は、最新のdumpをstdoutへcatするだけでいい。stdoutに書いた内容がそのままcontextへ注入される。
LATEST=$(printf '%s\n' "$DUMP_DIR"/session-*.md | sort -r | head -n 1)
[ -f "$LATEST" ] && cat "$LATEST"
最新ファイルの選定をls -tのmtime順ではなく、ファイル名(session-YYYYMMDD-HHMMSS.md)の辞書順にしている点がポイントだ。フォーマッタの一括適用などでファイルが軒並みtouchされると、mtime基準では古いdumpが「最新」に化けて誤注入されることがある。ファイル名に時刻を埋め込んでおけば、mtimeに依存せず時刻順を取れる。
まとめ: どういう場面で使うべきか
コンテキストウィンドウ圧縮は長時間セッションを続ける限り避けられない。モデルの自己要約だけに委ねると、直前の決定事項が「忘れた自覚もなく」欠け落ちる。ハーネス層のhookでPreCompact(書く)とSessionStart(読む)を分離する対策は、特定のワークフローに依存せず全セッションに自動で効くのが強みだ。ただし環境から機械的に取れる情報しか運べないという限界もあるため、正確な再開が必要な重要ワークフローにはアプリ層の状態管理と併用するのが実際のところ一番堅い。
さらに深掘りしたい方へ
この記事ではPreCompact/SessionStart hookパターンの選定理由と最小実装を解説しました。
AIが少し他人になる瞬間 — Claude Codeのコンテキストウィンドウ圧縮で消える文脈をhookで守る ではさらに:
- ツール実行履歴(権限リクエスト監査ログ)や決定事項メモまで退避対象を広げた完全な実装
-
settings.jsonでのcompact/startup/resume別のmatcher配線と、二重rotationの設計理由 - 別のガード系hookで見つかった迂回パターンの実例と、hookを長持ちさせる運用の勘所
を扱っています。
playpark について
playpark LLC - 業務自動化・AI活用・Web開発