Sougen Blog

Claude Code の文脈を 300k で区切って自動で再開する仕組みを作った

claudeaiworkflowherdr

Claude Code はターンを重ねるごとに文脈が大きくなり、リクエストのたびにその全体をキャッシュから読み直します。
大きくなるほど 1 リクエストあたりの費用が上がるので、膨らみすぎる前に区切ることにしました。
文脈を 300k で区切って引き継ぎメモを書き、/clear してから続きを自動で再開する仕組みです。
使っている hook とスクリプトは、公開用に整えて記事に添付しました。

文脈を大きくしすぎると何が高くつくか

どこで区切るのが得かを決めるために、300k に上げてから後の 33 セッション(うち 300k に到達したのは 13 セッション)を集計しました。

プロンプトキャッシュは、読み出しが入力の 0.05 倍(Opus 5.5 の場合)、1 時間 TTL の書き込みが 2 倍という料率です。
読み出しは安いのですが、文脈全体にリクエストのたびにかかるので、文脈が大きいほど固定費がじわじわ増えます。
一方で、区切るときにも一時費用がかかります。
/clear 直後の文脈は中央値で 86k ありました。
/checkpoint(記憶をファイルに書き出す自作のコマンドで、別の記事に書きました)と引き継ぎメモを書く目印が出てからセッションが終わるまでは約 9 リクエストで、どのリクエストも文脈全体を読みます。
その 9 リクエストに、/clear 後の最初の数リクエストで文脈を立て直すぶんを足すと、区切り 1 回は、通常のリクエスト 1 回の費用(中央値)のおよそ 20 回分でした(基本入力単価を 1 とした換算)。

リクエストごとの固定費と、この区切りの一時費用の釣り合いで、1 リクエストあたりの費用が最小になる、区切りまでの文脈の幅が決まります。
増分は 1 リクエストあたり中央値 1.5k で、/clear 直後の 86k から 300k に届くまでは中央値 131 リクエストでした。
作業中なら 40〜100 分ぶんにあたります。
なお目印はターンの境目でしか出ないので、実際の到達点が 320〜399k まで伸びることもありました(理由は後述します)。

200k ではなくあえて 300k で区切る

しきい値を変えながら、1 リクエストあたりの費用の最小点との差と、区切りの間隔を出しました。
間隔は、86k から 1 リクエスト 1.5k ずつ増えてしきい値に届くまでのリクエスト数を、作業中の速さ(約 1.7 リクエスト/分)で割って見積もっています。
300k なら約 143 リクエストで、実測の中央値 131 リクエストとおおむね合います。

最小点は 215k 付近でした。
区切り 1 回の費用 K は一定ではなく、しきい値 T の一次関数として増えます(実測で K ≈ 0.45T + 約 237k)。
目印が出てからの約 9 リクエストがどれも文脈全体を 0.05 倍で読むので(9 × 0.05 = 0.45)、しきい値を上げるほど 1 回の区切りが重くなるからです。
上の一覧は、この K を入れて計算したものです。
K が T とともに増えるぶん、最小点は 200k 台の前半にとどまりました。

300k にすると、ほぼ最小の 200k より費用は約 6% 高くなりますが、区切りの間隔は約 45 分ごとから約 84 分ごとに延びます。
区切り 1 回は自動なら 1〜3 分で終わるものの、200k だとひとつのまとまった作業の途中で何度もリセットがかかり、テンポが悪くなります。
そこで、費用が約 6% 増えるのを受け入れて 300k を選びました。
次に効果が大きいのは、しきい値を動かすことよりも、区切り 1 回の費用そのもの(目印以降のリクエスト数や、起動直後に積まれる 86k)を減らすことだと見ています。
試しに、30 日間使っていないスキル 23 本の自動発動と MCP 5 本を外したところ、/clear 直後の最初のリクエストは 73k から約 66k に減りました(1 回ずつの比較です)。

出力は入力の 5 倍の単価ですが、出力の量はしきい値によらないと仮定し、一覧の試算からは省いています。
そのぶん、一覧の差の % は出力を含めた総額での差より大きめに出ます。
サンプルは 300k に到達した 13 セッションと少なく、区切りで失われる文脈の質は数字に入っていません。

設定は Claude Code の settings.json の env に置きます。

"env": {
  "CLAUDE_CHECKPOINT_TOKENS": "300000",
  "CLAUDE_CODE_AUTO_COMPACT_WINDOW": "433000"
}

CLAUDE_CHECKPOINT_TOKENS が区切りのしきい値です。
CLAUDE_CODE_AUTO_COMPACT_WINDOW は安全網で、/clear が間に合わなかったときに自動 compact が受け止めます。
手元では、自動 compact は設定した値ちょうどではなく、その値から約 33k 手前で走りました。
設定を 300000 にしたときに 267k での発火を 2 回観測して、この 33k を割り出しました。
433000 なら、発火は 400k 前後になる見込みです。

保存して /clear で区切る

区切りは 2 つの hook が受け持ちます。

しきい値は、statusline の使用率ではなく実トークン数で判定します。
statusline の使用率は 1M のコンテキストウィンドウを基準にした割合なので、compact を早めに走らせる設定と組み合わせると、表示と実際の残りがずれるためです。
statusline を描くスクリプトが実トークン数を /tmp/claude-context-tokens-<session_id> にも書き出し、hook はこのファイルを読みます。

ひとつは UserPromptSubmit の hook で、しきい値を超えると残量を添えて通知します。
一度きりだとモデルが先送りして、そのまま自動 compact が走ってしまいます。
実際に 217k で通知したあと 267k で compact された日がありました。
そこでしきい値を超えたあとは 25k 刻みで再通知し続けます(送信自体は止めません)。

もうひとつは Stop の hook で、ターンを終えようとしたところを一度だけ止め、/checkpoint と引き継ぎメモの書き出しを指示します。
Stop の hook が終了コード 2 を返すと、Claude Code はターンの終了を取り消して続行を強制し、hook の標準エラー出力をモデルへ渡します。
文脈 1 周(しきい値超えから、/clear や compact でしきい値を下回るまで)につき 1 回だけ発火するよう、フラグファイルで抑えています。
この hook は、保存が済んだかどうかまでは確かめません。

添付したスクリプトは次の 6 本です。
statusline 以外は ~/.claude/hooks/ に置きます。
どれも jq を使い、worker は herdr と perl も使います。

settings.json には次のように登録します。
herdr を使わないなら、Stop の 2 つ目の要素は要りません。

"statusLine": { "type": "command", "command": "bash ~/.claude/statusline-tokens.sh" },
"hooks": {
  "UserPromptSubmit": [
    { "hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/context-bye-reminder.sh" }] }
  ],
  "Stop": [
    { "hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/stop-bye-trigger.sh" }] },
    { "hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/stop-auto-resume.sh" }] }
  ],
  "SessionStart": [
    {
      "matcher": "clear|compact",
      "hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/handoff-inject.sh" }]
    }
  ]
}

引き継ぎメモには、目的、作業中のファイル、未コミットの変更、決まったこと、次の一手を書きます。
置き場は ~/.claude/handoff/<起動ディレクトリの / を - にした名前>.md で、作業ディレクトリごとに 1 ファイルになります。
途中で Bash が cd しても名前が変わらないよう、hook に渡る cwd より CLAUDE_PROJECT_DIR を優先しています。
同じディレクトリで並行して走るセッションは同じファイルを共有するので、片方のメモをもう片方が上書きすることがあります。

/clear の直後にメモを読み込ませる

/clear しただけでは、次のセッションは直前の会話を知らない状態で「次は何をするか」を聞き返してきます。
そこで、SessionStart の hook で引き継ぎメモを読み込ませます。
matcher を clear|compact にして、文脈リセットの直後にだけ走らせます。

この hook は引き継ぎメモを読み、additionalContext として新しい文脈に注入します。
読んだメモは used/ へ退避して二度注入を防ぎます。
更新から 6 時間を超えたメモは別作業の残骸とみなし、注入せずに used/ へ移します。
保存はモデルがファイルに書き、復元は起動時に hook が注入する、という分担です。

herdr で /clear と再開まで自動化する

ここまででも、ユーザーが /clear と打てば続きから再開できます。
ただ、自宅ラボ環境では複数のエージェントを常駐させていて、それぞれに /clear を打って回るのは手間です。
そこで、エージェント向けのターミナルマルチプレクサ herdr の pane で動いているときは、/clear の送信と再開の指示も自動化しました。

受け持つのは、設定例の Stop の 2 つ目にある stop-auto-resume.sh です。
この hook は、herdr 上(環境変数 HERDR_ENV=1 と HERDR_PANE_ID で判定)でしきい値を超え、かつ引き継ぎメモが書かれているときだけ動きます。
しきい値を超えて最初にターンを終えようとしたときは、まだメモがないので、stop-bye-trigger.sh が保存を指示するだけです。
メモを書き終えてもう一度ターンを終えようとしたときに、この hook が worker スクリプトを nohup で切り離して起動します。
worker を切り離すのは、ターンが終わって pane が待機状態になるのを待つためです。
hook の中で待つと、hook が終わるまでターンも終わらないので、いつまでも待機状態になりません。

worker の仕事は次の順です。
pane が待機状態になって入力欄が空くのを待ち、自分の pane へ herdr agent prompt <pane> "/clear" を送ります。
/clear が効いたかどうかは、元の場所から引き継ぎメモがなくなったかで判定します。
handoff-inject.sh がメモを used/ へ移すと、この条件がそろいます。
メモがなくなったら、再開の指示を送ります。

止めたいときは、メモの拡張子 .md を .noauto に替えた名前のファイルを置きます。
この印は次の /clear か compact で消えるので、止まるのはその 1 周だけです。
ずっと止めておきたいときは、CLAUDE_AUTO_RESUME=0 を立てます。
ユーザーの返事待ちなど区切りが悪いときは、モデル自身に .noauto を touch させて見送らせます。

ハマったところ

しきい値を見る UserPromptSubmit と Stop の hook は、プロンプト送信時とターン終了時にしか動きません。
AskUserQuestion で選択肢を出して回答を受け取る往復は、どちらの境目にもなりません。
/clear 直後のバックグラウンド通知からそのまま走り出し、途中の判断を AskUserQuestion で取り続けたら、1 ターンが終わらないまま文脈が 369k まで伸びました。
今の構成では、長く自走するときは、300k を超えたら自分から /checkpoint と引き継ぎメモを書いてターンを区切る必要があります。

/clear の直後は Enter が取りこぼされます。
新しい文脈を準備している最中に文字を送ると、本文は入力欄に入るのに Enter が効かず、送信されずに残ることがあります。
worker は再開の指示を送ったあと、入力欄が空になったかを確かめ、残っていれば Enter を最大 5 回まで送り直します。

入力欄が空かどうかの判定にも癖があります。
空の入力欄には次に打つ内容のサジェスチョンが薄字で表示されるので、素朴に読むと「何か入っている」と誤判定します。
pane を ANSI つきで読み、薄字(SGR 2)の範囲を消してから、罫線 2 本に挟まれた行を見ます。

まだ信頼していないフォルダで使い捨ての pane を開き、Claude を起動すると、最初に「このフォルダを信頼するか」を聞く画面が出て、そこで止まります(herdr からは blocked に見えます)。
手元では、親フォルダを信頼済みにしていても、子フォルダで起動した pane でこの画面が出ました。
worker はこの blocked を検知すると、自動再開を見送って手元に通知を飛ばします。
入力欄が空かないまま時間切れになったときや、/clear が効かなかったときも同じです(添付版では、通知に使うコマンドを AUTO_RESUME_NOTIFY_CMD で渡します)。

こうした例外を除けば、文脈は 300k を超えるたびに区切られ、引き継ぎメモから続きが再開されます。
常駐させているエージェントに /clear を打って回らなくても、1 リクエストあたりの費用が膨らみすぎる前に文脈が軽くなります。