私が Claude Code 向けに書いた状態バー cs / claude-statusbar には、cache 4m23s という行がある。緑色で、毎秒カウントダウンし、終わりまで進むと赤色の cache COLD になる。
ある人に聞かれた。この数字は結局どう計算しているのか、正確なのか?
聞く価値のある質問だ。Pro / Max の契約ユーザーにとって、キャッシュがヒットしたとき、その部分の context は基本的に 5h / 7d の上限をほとんど消費しない。冷ましてしまうと、次の prompt では全体のコンテキストを定価で最初から再投入することになる。だから「あと何分」というこの一行は、「今、熱いうちにもう一通送るべきか」を決めるものだ。以下でそれを分解しつつ、正確なのかにも答える。
急いでいる人向けに一言でいうと、標準設定、5 分キャッシュでは正確だ。ただし、体系的に誤って見せる唯一のケースは、1 時間キャッシュを有効にしているのに TTL を変更していないとき——この場合は 55 分早く通知する。 一行の設定で直せる。理由は下で説明する。
まず 2 つの「cache」を区別しよう。混同しない
このリポジトリには cache と呼ばれるものが 2 つある。「正確かどうか」を問う前に、どちらのことを聞いているのかを確認する必要がある。
- データキャッシュ:
cache.pyのCACHE_MAX_AGE_S = 30。claude-monitorの出力を 30 秒キャッシュするものだ。これは純粋に、ステータスバーが毎秒再描画されるたびに、毎回サブプロセスを shell で叩かなくてすむようにするためのもの。 「残り時間は正確か」とは関係ない。 - prompt-cache の残り時間:今日の主役。これは「Anthropic のプロンプトキャッシュがあとどれくらいで期限切れになるか」を計算している。
以下では 2 つ目だけを扱う。
どこを基準にしているか
ロジックはかなり短く、関数は 1 つだけ、get_cache_age_text だ。やっていることは 3 つ。
~/.cache/claude-statusbar/last_stdin.jsonを読み、現在のセッションのtranscript_pathを取る。- この JSONL を後ろから読み、直近の
type == "assistant"のレコードを探し、そのtimestampを取る。 remaining = ttl_seconds - <ruby>経過<rt>けいか</rt></ruby>した<ruby>秒数<rt>びょうすう</rt></ruby>として、カウントダウンの形にフォーマットする。
2 番目は _last_assistant_age で、要点はこの 1 行だけ。
if entry.get("type") != "assistant":
continue
...
return (datetime.now(timezone.utc) - last_ts).total_seconds()
基準点に注意:直近の assistant メッセージのタイムスタンプ——ユーザーメッセージでもなく、ファイルの mtime でもない。この選択は正しい。なぜかは次の節で説明する。
式も同じく素直だ。
remaining = ttl_seconds - age_s
if remaining <= 0:
return "COLD"
ttl_seconds はデフォルトで 300。remaining <= 0、またはそもそも assistant レコードが見つからない(age_s is None)場合は、どちらも COLD を返す。transcript_path すらない場合は空文字列を返し、この表示ブロック全体が隠れる。
ついでに歴史を少し:v3.2.2 のこの PR より前は、この行が「どれくらい経過したか」(elapsed)を表示していたが、あとでカウントダウン(countdown)に変わった。ユーザーが本当に知りたいのは「前回の回答から何分経ったか」ではなく、「キャッシュが死ぬ前に、まだもう 1 通送る時間があるか」だからだ——カウントダウンなら直接答えてくれるし、elapsed だと自分で頭の中で引き算しなければならない。
Anthropic の実際の挙動をちゃんとモデル化できているか
公式ドキュメント Prompt caching を見ると、方向を決める文が 2 つある:
By default, the cache has a 5-minute lifetime.
The cache is refreshed for no additional cost each time the cached content is used.
つまり、TTL はスライディングウィンドウだ。ヒットするたびに 5 分へリセットされる。
これは、「直近の 1 回の assistant をアンカーにする」のがなぜ正しいかをちょうど説明している——1 回回答が増えるたびに、age_s はゼロから数え直しになり、カウントダウンは自動で満タンまで延長され、サーバー側の「1 回使うたびに 1 回延長」という挙動と一致する。コード内のあのコメント # 5min — Anthropic's default prompt cache TTL は間違っていない。この層では、モデル化はちゃんと合っている。
どこが不正確か——証拠を出す
ここからが本題。3 つの層を、いちばん刺さるものから、いちばんどうでもいいものへ並べる。
1. デフォルト TTL が 5 分で固定されているが、あなたは 1 時間キャッシュで動いているかもしれない
ここだけが、本当に人をだますところ。証拠は、手元の直近 assistant レコードの usage ブロックから:
"cache_creation": {
"ephemeral_1h_input_tokens": 1421,
"ephemeral_5m_input_tokens": 0
}
全部 1 時間バケットに入っている。つまりこのマシンで実際に走っているのは 1h キャッシュで、本当の生存時間は 60 分。でも cs のデフォルトは cache_ttl_seconds = 300 なので、5 分後には cache COLD と叫ぶ——真実より 55 分も早い。
いちばん皮肉なのは、5m か 1h かを判定する「真実のシグナル」(ephemeral_1h_input_tokens vs ephemeral_5m_input_tokens)が、ツールがすでに開いている同じファイル、同じレコードの中に転がっていることだ。なのに _last_assistant_age は type と timestamp の 2 フィールドだけを読み、その usage ブロックをそのまま素通りしている。理論上は transcript からどの TTL を使うべきか自動判定できるのに、今は手動で cs config set cache_ttl_seconds 3600 する必要がある。これは埋めるべき TODO だ。
2. アンカーは「1 ラウンドの終了」であって、「キャッシュが更新されたその瞬間」ではない
assistant の timestamp は、おおむねそのラウンドを**書き終えた**時刻だ。一方、キャッシュはリクエストが**送信された**瞬間にサーバー側で更新される。その間には生成遅延がある。実際の transcript にある、同じ会話セグメントの assistant タイムスタンプを見ると:
assistant 2026-05-29T04:46:18.432Z
assistant 2026-05-29T04:46:19.653Z
assistant 2026-05-29T04:46:25.680Z
数秒から十数秒のオーダー。300s / 3600s の TTL と比べれば、無視できる。方向としてはたぶん楽観寄り(表示される残りが、実際のサーバー側より少し多い)だが、人を噛むほどではない。
ここは正直に言うと、源码だけでは Anthropic サーバー側がリクエスト開始から数えるのか、終了から数えるのかは証明できない。だから正確な言い方は——**アンカーは「1 ラウンドの遅延精度」の代理値**であって、キャッシュ更新の正確な瞬間ではない。実用には十分。でもストップウォッチだとは思わないほうがいい。
3. 色は数値ではなく文字列を見て推測している
おもしろい工学上の割り切り。_cache_severity は残り秒数を受け取らず、**すでにフォーマット済みの文字列**を受け取り、その中に m / h があるかを見る:
if cache_text == "COLD":
return theme.s_hot # 赤
if "m" in cache_text or "h" in cache_text:
return theme.s_ok # 緑、快適ゾーン
return theme.s_warn # 黄、純粋な "Ys"、1 分未満
1 分未満のとき、formatter はわざと m なしの裸の Ys だけを出力する。colorizer が「そろそろ黄にする時間だ」と検出できるようにするためだ。formatter と colorizer の間には暗黙の契約がある。リポジトリには、この契約を固定する test_cache_severity.py がわざわざあり、いつかフォーマットを変えたときに色がこっそり混線しないようにしている。使えるが、たしかに結合ではある——知っておく価値はある。
もうひとつ端の話:transcript の逆読みには 320KB の上限(10×32KB) がある。巨大な transcript で、末尾 320KB に assistant が見つからなければ、そのまま COLD 扱いになる。これは性能上の割り切りだ——ステータスバーは毎秒再描画されるので、毎回 数 MB を走査するわけにはいかない。日常ではまず踏まない。
それで、正確なのか
- 5 分キャッシュ + デフォルト設定:正確。アンカーは正しいし、スライディングウィンドウのモデリングも正しい。境界ケース(時計の巻き戻りは 0 にクランプ、naive タイムスタンプは UTC 扱い、
Z接尾辞の正規化)も処理されている。 - 1 時間キャッシュ + TTL 未変更:構造的に 55 分早く報告する。
cs config set cache_ttl_seconds 3600の 1 行で直せる。 - 秒単位の精度:期待しないこと。アンカー自体に 1 ラウンド分の遅延という代理誤差がある。これは「あと何分残っているか」級のヒントであって、計時器ではない。
一言でまとめると、これは「キャッシュがまだ熱いうちに、いまもう 1 発投げるべきか」に答えるものだ。この問いにはかなり正確に答える。ストップウォッチとして使うなら、それは道具を間違えている。
自分で見るなら、_last_assistant_age と get_cache_age_text の 2 つの関数から入るといい。30 行ほどで読み終わる。
コメント
コメントは即時公開されますが、ポリシー違反時は非表示になる場合があります。