# ステータスバーのあの cache 4m23s は、結局正確なのか？

私が Claude Code 向けに書いた{状態|じょうたい}バー [`cs` / claude-statusbar](https://github.com/leeguooooo/claude-code-usage-bar) には、`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`](https://github.com/leeguooooo/claude-code-usage-bar/blob/26b7475f4a0c885450f29e62b64a393562be9962/src/claude_statusbar/cache.py#L49)。`claude-monitor` の{出力|しゅつりょく}を 30 {秒|びょう}キャッシュするものだ。これは{純粋|じゅんすい}に、ステータスバーが{毎秒|まいびょう}{再描画|さいびょうが}されるたびに、{毎回|まいかい}サブプロセスを shell で{叩|たた}かなくてすむようにするためのもの。 「{残|のこ}り{時間|じかん}は{正確|せいかく}か」とは{関係|かんけい}ない。
- **prompt-cache の{残|のこ}り{時間|じかん}**：{今日|きょう}の{主役|しゅやく}。これは「Anthropic のプロンプトキャッシュがあとどれくらいで{期限切|きげんぎ}れになるか」を{計算|けいさん}している。

{以下|いか}では 2 つ{目|め}だけを{扱|あつか}う。

## どこを{基準|きじゅん}にしているか

ロジックはかなり{短|みじか}く、関数は 1 つだけ、[`get_cache_age_text`](https://github.com/leeguooooo/claude-code-usage-bar/blob/26b7475f4a0c885450f29e62b64a393562be9962/src/claude_statusbar/core.py#L964-L1030) だ。やっていることは 3 つ。

1. `~/.cache/claude-statusbar/last_stdin.json` を{読|よ}み、{現在|げんざい}のセッションの `transcript_path` を{取|と}る。
2. この JSONL を{後|うし}ろから{読|よ}み、**{直近|ちょっきん}の** `type == "assistant"` のレコードを{探|さが}し、その `timestamp` を{取|と}る。
3. `remaining = ttl_seconds - {経過|けいか}した{秒数|びょうすう}` として、カウントダウンの{形|かたち}にフォーマットする。

2 {番目|ばんめ}は [`_last_assistant_age`](https://github.com/leeguooooo/claude-code-usage-bar/blob/26b7475f4a0c885450f29e62b64a393562be9962/src/claude_statusbar/core.py#L901-L961) で、{要点|ようてん}はこの 1 {行|ぎょう}だけ。

```python
if entry.get("type") != "assistant":
    continue
...
return (datetime.now(timezone.utc) - last_ts).total_seconds()
```

{基準点|きじゅんてん}に{注意|ちゅうい}：**{直近|ちょっきん}の assistant メッセージのタイムスタンプ**——ユーザーメッセージでもなく、ファイルの mtime でもない。この{選択|せんたく}は{正|ただ}しい。なぜかは{次|つぎ}の{節|せつ}で{説明|せつめい}する。

{式|しき}も{同|おな}じく{素直|すなお}だ。

```python
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](https://github.com/leeguooooo/claude-code-usage-bar/pull/20) より{前|まえ}は、この{行|ぎょう}が「どれくらい{経過|けいか}したか」（elapsed）を{表示|ひょうじ}していたが、あとでカウントダウン（countdown）に{変|か}わった。ユーザーが{本当|ほんとう}に{知|し}りたいのは「{前回|ぜんかい}の{回答|かいとう}から{何分|なんぷん}{経|た}ったか」ではなく、「キャッシュが{死|し}ぬ{前|まえ}に、まだもう 1 {通|つう}{送|おく}る{時間|じかん}があるか」だからだ——カウントダウンなら{直接|ちょくせつ}{答|こた}えてくれるし、elapsed だと{自分|じぶん}で{頭|あたま}の{中|なか}で{引|ひ}き{算|ざん}しなければならない。

## Anthropic の{実際|じっさい}の{挙動|きょどう}をちゃんとモデル{化|か}できているか

{公式|こうしき}ドキュメント [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/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`](https://github.com/leeguooooo/claude-code-usage-bar/blob/26b7475f4a0c885450f29e62b64a393562be9962/src/claude_statusbar/config.py#L22) は{間違|まちが}っていない。この{層|そう}では、モデル{化|か}はちゃんと{合|あ}っている。

## どこが{不正確|ふせいかく}か——{証拠|しょうこ}を{出|だ}す

ここからが{本題|ほんだい}。3 つの{層|そう}を、いちばん{刺|さ}さるものから、いちばんどうでもいいものへ{並|なら}べる。

### 1. デフォルト TTL が 5 {分|ふん}で{固定|こてい}されているが、あなたは 1 {時間|じかん}キャッシュで{動|うご}いているかもしれない

ここだけが、本当に{人|ひと}をだますところ。{証拠|しょうこ}は、{手元|てもと}の{直近|ちょっきん} assistant レコードの `usage` ブロックから：

```json
"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`](https://github.com/leeguooooo/claude-code-usage-bar/pull/9) する{必要|ひつよう}がある。これは{埋|う}めるべき TODO だ。

### 2. アンカーは「1 ラウンドの{終了|しゅうりょう}」であって、「キャッシュが{更新|こうしん}されたその{瞬間|しゅんかん}」ではない

assistant の `timestamp` は、おおむねそのラウンドを**{書|か}き{終|お}えた**{時刻|じこく}だ。一方、キャッシュはリクエストが**{送信|そうしん}された**{瞬間|しゅんかん}にサーバー{側|がわ}で{更新|こうしん}される。その{間|あいだ}には{生成遅延|せいせいちえん}がある。{実際|じっさい}の transcript にある、{同|おな}じ{会話|かいわ}セグメントの assistant タイムスタンプを{見|み}ると：

```text
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`](https://github.com/leeguooooo/claude-code-usage-bar/blob/26b7475f4a0c885450f29e62b64a393562be9962/src/claude_statusbar/styles.py#L40-L56) は{残|のこ}り{秒数|びょうすう}を{受|う}け{取|と}らず、**すでにフォーマット{済|ず}みの{文字列|もじれつ}**を{受|う}け{取|と}り、その{中|なか}に `m` / `h` があるかを{見|み}る：

```python
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）](https://github.com/leeguooooo/claude-code-usage-bar/blob/26b7475f4a0c885450f29e62b64a393562be9962/src/claude_statusbar/core.py#L897-L898) がある。{巨大|きょだい}な 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`](https://github.com/leeguooooo/claude-code-usage-bar/blob/26b7475f4a0c885450f29e62b64a393562be9962/src/claude_statusbar/core.py#L901-L961) と [`get_cache_age_text`](https://github.com/leeguooooo/claude-code-usage-bar/blob/26b7475f4a0c885450f29e62b64a393562be9962/src/claude_statusbar/core.py#L964-L1030) の 2 つの{関数|かんすう}から{入|はい}るといい。30 {行|ぎょう}ほどで{読|よ}み{終|お}わる。
