コンテンツにスキップ

起動直後にトークン表示が消える問題 (2026-08-05)

ユーザー報告: 「プロジェクトを立ち上げて初めてAgentPanelを開くと履歴のトークンが無しの状態」。

1. 実測(サンドボックスでの2プロセス実験)

Section titled “1. 実測(サンドボックスでの2プロセス実験)”

「起動直後」は本物でしか再現できないので、既知値(合計12345/6789、モデル別777/888)の キャッシュを植えるプロセスと、それを読む別の新しいプロセスに分けて測った。

起動直後の状態(修正前)結果
セッション合計(ステータスバーの「19.1k tok」)正しく復元される
AgentHub.LastModelUsage0件

つまり症状は「合計は出るのにモデル別が全滅」という非対称。LastModelUsage は 使用状況ポップオーバーの行とコンテキストメーターの唯一の供給源なので、 起動直後は両方が空になり、次のターン完了まで戻らない。

_lastModelUsage は素の static で、エディタプロセスと共に消える。復元経路は2つあるのに:

  • 履歴からの復元(v0.15.0): トランスクリプトから TranscriptUsage.LastTurnModelUsage を再構成して復元する — 動く。
  • ただの起動: SessionCache.Load() はセッション合計だけを持っている — モデル別が無い。

v0.15.0 が「履歴を読み込むと使用状況が出ない」を直したとき、キャッシュ起動側には 同じ修正が及んでいなかった。

キャッシュにモデル別スナップショットを持たせる:

  • ModelUsage.ToJson() を FromJson の隣に追加(ラウンドトリップでテスト可能に)
  • SessionCacheFile.Save(session, modelUsage) が lastModelUsage キーを書く (null/空なら書かない = 旧フォーマットと同形)
  • SessionCacheFile.Load(out modelUsage) が復元(キー無し = 空辞書、null は返さない)
  • AgentHub.Session の遅延ロードで _lastModelUsage も復元
  • AgentHub 内の全 Save 呼び出し(10箇所)が現在の辞書を運ぶ

旧キャッシュ(キー無し)は「まだターンが無い」と同じ扱いに落ちるだけで、移行処理は不要。

  • 同じ2プロセス実験を修正後に再実行: 新プロセスの LastModelUsage に claude-probe-model: in=777 out=888 ctx=200000 が復元された。
  • ラウンドトリップ+旧フォーマット読みの回帰テストを SessionCacheFileTests に追加。
  • サンドボックス 2014 tests / 0 failed。

なお実験に使ったサンドボックス自身のキャッシュが空セッション(totals 0)だったことも 観測した。これは e2e テストの後始末による正常な状態で、今回の症状とは別物。

5. 実地スクリーンショットでの追跡 (2026-08-05)

Section titled “5. 実地スクリーンショットでの追跡 (2026-08-05)”

v0.20.1 配備後、ユーザーのスクリーンショットで「2.0kトークン」の隣に 「まだ完了したターンがありません。」が出ている状態を実測で追跡した:

  • 実行中のコードは v0.20.1(ToJson あり)
  • しかしディスク上のキャッシュは v0.20.0 が最後に書いたもので lastModelUsage キーが無い
  • → 復元は正しく「空辞書」に落ち、ポップオーバーは空表示

つまり設計どおりのアップグレード直後の一回きりの状態(次のターン完了で自己解消)だが、 空表示の文言「まだ完了したターンがありません」は、すぐ上に合計とターン数が出ている状態では 事実に反する。空状態には2つの真実(本当にターンが無い / 内訳だけが無い)があり、 メッセージはどちらであるかを見て選ぶ必要がある。

修正: completedTurns > 0 || totals > 0 のとき 「合計はセッションキャッシュから復元済みです。モデル別の内訳は、次のターン完了後に表示されます。」 を出す(StatusUsagePopoverBreakdownPending)。本番で選択ロジックが同文言を返すことを確認済み。

トランスクリプトからの一回きりのバックフィル(起動時に JSONL を読んで内訳を再構成)は 却下 — 1ターンで自己解消する状態のために起動時のファイル走査を足す価値がない。

6. 却下の撤回: 起動時バックフィル (2026-08-05)

Section titled “6. 却下の撤回: 起動時バックフィル (2026-08-05)”

セクション5で「1ターンで自己解消するのでトランスクリプトからのバックフィルは不要」と 却下したが、ユーザーの指摘で撤回した:

でも、一旦ほかの履歴に切り替えてから戻すと表示されますよ?

そのとおりで、履歴切り替え(SwitchToSession)はトランスクリプトから内訳を再構成する。 つまりデータはディスクにあり、再構成コードも既にあり、起動パスだけが使っていなかった。 回避策をユーザーが自力で発見して使っている時点で、「待てば直る」は設計判断として誤り。

実装: 起動復元でキャッシュに内訳が無く、かつセッションに実績(ターン/トークン)がある場合、 History と同じ経路(SessionIndex の cwd 変換 + sessionId.jsonl)でトランスクリプトを解決し、 SwitchToSession と同じ再構成を行い、結果をキャッシュに保存する。これにより:

  • 走査は「内訳が欠けている起動」でしか起きない(新規プロジェクトは即スキップ)
  • 保存により次回の起動は高速パス — 定常状態のコストはゼロ
  • セクション5の「毎起動のファイル走査」という却下理由は、そもそも成立していなかった

検証: 報告のセッションそのもの(1ターン、キー無しキャッシュ)に対し、配備のドメイン リロード直後、新しいターン無しで claude-opus-5 の内訳が復元され、キャッシュに lastModelUsage キーが書かれたことを実測。回帰テスト3件 (AgentHubModelUsageBackfillTests)。

既知の制限(既存の履歴復元と同一): トランスクリプト JSONL は contextWindow を持たない (v0.15.0 で実測)ため、バックフィルされた内訳の ContextWindow は 0。ポップオーバーの 行は出るが、コンテキストメーターの割合表示は次のライブターンまで出ない場合がある。