0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

5hブロック上限を踏む前に知る ― token-budget-advisorとステータスライン埋め込み

0
Posted at

前作「レートリミットで止まったセッションを自動再開する」では5hブロックに止められた後の話を書きました。今回は止まる前に気づく仕組みです。

Claude MAXの5hブロックは出力トークンが上限に達した瞬間にセッションが止まります。ステータスラインに 🕐 5h 82% が見えていても、残り18%が何トークン分かは分かりません。**800k近くで「そろそろやばい」、1.2M超で「もう詰んでいる」**という絶対値での境界を検出するのが token-budget-advisor.sh で、この記事ではその設計と dashboard.sh への統合、ステータスラインへの配線を書きます。

困りごと:上限を踏むまで気づけない

Claude Codeのステータスラインには 🕐 5h N% が表示されます。~/.claude/scripts/statusline.sh はこれをClaudeのstdinから直接読んでいます。

H5=$(j '.rate_limits.five_hour.used_percentage // empty')
H5R=$(j '.rate_limits.five_hour.resets_at // empty')

pcolor 関数が50%以上で黄、80%以上で赤に色を変えますが、これはClaudeが計算したパーセンテージであって、絶対トークン数ではありません。80%が何tokに相当するかはMaxプランの契約状態次第で変わり、長い補完を続けると上限に到達して次のターンから突然止まります。

確認するには毎回 ccusage blocks を叩く必要があり、それを忘れて重いタスクを流すと5h枠をきれいに使い切ります。無人ジョブが裏で動いている場合、人間が気づく前に全SKIP状態になる。

token-budget-advisor.sh の設計

~/.claude/scripts/token-budget-advisor.sh のヘッダにある方針がそのまま設計になっています。

# しきい値:
#   5h block:  output > 800k  → warn (>1.2M で critical)
#   weekly:    cost  > $3000  → warn
#   sessions:  >5/day         → warn (集中作業の疑い)
#
# 失敗時は exit 0 で fail-open (dashboard 統合のため)
# bash 3.2 互換

デュアルソース:ccusage を1次、cost-log.jsonl を2次

まず ccusage blocks --json を試みます。

if command -v ccusage >/dev/null 2>&1; then
  CC_JSON=$(ccusage blocks --json 2>/dev/null || true)
  if [ -n "$CC_JSON" ]; then
    EXTRACTED=$(printf '%s' "$CC_JSON" | python3 -c "
import sys, json
try:
    d = json.load(sys.stdin)
    active = [b for b in d.get('blocks', []) if b.get('isActive')]
    if active:
        b = active[0]
        tc = b.get('tokenCounts', {}) or {}
        out = int(tc.get('outputTokens', 0))
        cost = float(b.get('costUSD', 0))
        print(f'{out}|{cost}')
    ...")

ccusage が居ない環境は cost-log.jsonl だけで回ります。両方ある場合は ccusage の値を優先し、差分を source_diff_pct として記録します。

# ccusage の値が有効ならそちらを優先 (transcript 計算より信頼できる)
own_out_5h = out_5h
if cc_out is not None and cc_out > 0:
    out_5h = cc_out

# 比較 (検証用)
diff_pct = None
if cc_out is not None and own_out_5h > 0:
    diff_pct = round(abs(cc_out - own_out_5h) / max(cc_out, own_out_5h) * 100, 1)

差が大きい時はcost-log側の集計ロジックに問題がある可能性があり、source_diff_pct が目安になります。

cost-log.jsonl は「最新行のみ採用」しないと数値が2〜3倍になる

cost-log.jsonl は同じ (session_id, transcript) キーで複数行書かれます(セッション途中の累積値が追記される仕様)。全行を合算するとトークンが二重カウントされます。

# cost-log は session_id × transcript ごとに累積値で書かれる仕様。
# 最新行のみ採用するため、(session_id, transcript) で最終行を取り直す。
latest = {}
with open(log_path) as f:
    for line in f:
        r = json.loads(line)
        key = (r.get("session_id", ""), r.get("transcript", ""))
        prev = latest.get(key)
        if (prev is None) or (t > prev[0]):
            latest[key] = (t, r)

最終行だけ採用してから5h/7d窓でフィルタします。これを知らずに全行合算すると、実際の2〜3倍の数値が出ます。

しきい値と判定ロジック

THRESH_5H_WARN     = 800_000      # output tokens
THRESH_5H_CRIT     = 1_200_000
THRESH_WEEK_WARN   = 3000         # USD
THRESH_SESS_PER_DAY = 5

if out_5h >= THRESH_5H_CRIT:
    s5 = "critical"
elif out_5h >= THRESH_5H_WARN:
    s5 = "warn"
else:
    s5 = "ok"

集中作業判定は「直近3日平均 > 5 sess/day」で出します。

recent_days = sorted(by_day.keys())[-3:]
avg_sess = sum(by_day[d] for d in recent_days) / max(1, len(recent_days))
burst = avg_sess > THRESH_SESS_PER_DAY

--short モードの出力フォーマット

_short フィールドは Python 側で生成されます。

"_short": f"{icon} {label} (5h:{out_5h/1000:.0f}k tok ${cost_5h:.1f} / 7d:${cost_7d:.0f})",

状態ごとの出力例(実際のトークン数は変わります)。

🟢 OK (5h:234k tok $0.3 / 7d:$2)
🟡 burst (5h:841k tok $1.2 / 7d:$8)
🔴 cap-near (5h:1243k tok $2.1 / 7d:$12)

アイコンだけ grep -qE '🔴|cap-near' できるのでシェルスクリプトのゲートに直接挿せます。エラー時は ⚫ n/a を出して exit 0 で返るため(fail-open 設計)、呼び出し元が止まりません。

ステータスラインへの埋め込みと dashboard.sh 統合

dashboard.sh の Cost セクションへ組み込む

~/.claude/scripts/dashboard.sh## 💰 Cost (7d) セクションがこうなっています。

echo "## 💰 Cost (7d)"
~/.claude/scripts/cost-summary.sh --short
echo "  budget: $(~/.claude/scripts/token-budget-advisor.sh --short)"

~/.claude/dashboard.md に書き出されるので、プロジェクト開始時にファイルを開くだけでbadget状況が分かります。

launchd で4時間ごとに自動更新

~/Library/LaunchAgents/com.shun.dashboard.plist の設定です。

<key>Label</key>
<string>com.shun.dashboard</string>
<key>StartInterval</key>
<integer>14400</integer>

14400 秒 = 4時間。macが起きている間は4時間に一度 dashboard.sh が走り、budget 行が自動で書き換わります。手動で ccusage blocks を叩かなくても、ファイルを見れば直近の集計値が確認できます。

無人ジョブのゲートとして使う

autopilotのような無人スクリプトでは --short の出力でジョブを事前停止します(claude-autopilot 記事で紹介した配線)。

BUDGET=$(~/.claude/scripts/token-budget-advisor.sh --short)
if echo "$BUDGET" | grep -qE '🔴|critical|cap-near'; then
  log "ABORT: budget critical"; exit 0
fi

ラベル判定だけだと境界付近でギリギリ素通りするケースがあります。クリティカルな判定をする場合は、JSON出力(--short なし)の 5h_output_tokens フィールドを数値で取り出して >= 1200000 を直接チェックする方が確実です。

踏んだ落とし穴

  • cost-log.jsonl を全行合算して数値が2〜3倍になった(session_id, transcript) キーで最終行だけ採用する方式に変更
  • ccusage がPATHに居ない環境でスクリプトがそのまま止まったcommand -v ccusage で存在確認し、なければ cost-log.jsonl 単独で動く fallback パスを持つ
  • fail-open にしていないと dashboard.sh ごと落ちる → エラー時は exit 0 + ⚫ n/a 出力にして、dashboard の他セクションを守る
  • launchd の最小PATHで ccusage が見つからないProgramArguments/bin/zsh -c 経由にしてzshのパスを引く
  • burst フラグが週末の集中作業で常時点灯する → 直近3日平均 > 5 sess/day の判定なので、用途によっては THRESH_SESS_PER_DAY を引き上げる
  • statusline.sh の rate_limits が初回ターン前は -- になる → Claudeが「サブスクリプション契約者の最初のAPI応答後」にだけ rate_limits をstdinに含めるため。最初のターン後は正常に表示される

まとめ

  • 5hブロックの上限は「止まってから」でなく、800k / 1.2M の絶対値で事前に検出する
  • token-budget-advisor.sh は ccusage(1次)と cost-log.jsonl(2次)のデュアルソースで集計し、差分を source_diff_pct で可視化する
  • cost-log は (session_id, transcript) キーの最終行だけ採用しないと2〜3倍の誤計算になる
  • --short モードは 🟢/🟡/🔴 1行に圧縮し、シェルのゲートや dashboard 埋め込みに直接使える
  • dashboard.sh が launchd の StartInterval 14400(4h)で自動更新し、budget 行が常に最新に保たれる
  • 失敗時は fail-open(exit 0 / ⚫ n/a)にしておくと、dashboard 全体の落下を防げる

次回は、このダッシュボードが参照する cost-log.jsonl のログローテーションと、集計が重くなる前に古いエントリを圧縮する話を書きます。


Lily@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています

皆さんの ❤️ やシェアが励みになります!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?