Skip to content

fix(si): Claude 5 era catch-up — pricing/window tables, subagent usage backfill, stdin-first quota, statusline usage row - #7

Merged
dayakarreddyn merged 12 commits into
mainfrom
fix/si-claude5-pricing-usage-backfill
Jul 17, 2026
Merged

fix(si): Claude 5 era catch-up — pricing/window tables, subagent usage backfill, stdin-first quota, statusline usage row#7
dayakarreddyn merged 12 commits into
mainfrom
fix/si-claude5-pricing-usage-backfill

Conversation

@dayakarreddyn

Copy link
Copy Markdown
Owner

Summary

Full-stack audit (2026-07-17) found the model-upgrade wave had outrun SI's tables, the background-agent default had blinded the tracker, and the usage endpoint had become unpollable. 11 commits, all verified live while dogfooding.

Telemetry correctness

  • Pricing tables (lib/cost-estimation.js): add Claude 5 family (claude-fable/claude-mythos at $10/$50); reprice Opus 4.5+ to $5/$25 (rows were 3x overstated — live DB total corrected $1,122.74 → $374.25 by a user_version 1→2 migration recomputing from stored tokens); Sonnet 5 introductory $2/$10 with a dated flip on 2026-09-01; per-turn model pricing in both transcript accumulators (cost caches v2→v4).
  • Subagent usage backfill (events.reconcileSubagentUsage, wired into si-bootstrap): 54% of agent_invocations rows had NULL model/tokens/cost — background-default agents fire PostToolUse before their transcript flushes usage. Claude Code 2.1.x also dropped parentToolUseId from subagent transcripts, so pairing now comes from the parent transcript (launch-ack agentId: text + task-notification <task-id>/<tool-use-id> markers via mapAgentToolUseIds). Live run backfilled 403 blind rows.

Usage quota (the "missing percentages" outage)

  • /api/oauth/usage aggressively 429s (anthropic/claude-code#31637, closed not-planned; observed Retry-After up to 3600s, and ~1 req/hour tolerance). Root fix: source rate_limits from statusline stdin (CC ≥2.1.90) — fresh every redraw, zero API calls. The oauth poller remains as fallback only, now honoring Retry-After with a 5-minute error floor and carrying last-good values through outages (rendered with a ~ stale marker).
  • contextCap prefers stdin context_window.context_window_size; falls back to family regex incl. fable/mythos → 1M.

Statusline

  • Line 2 leads with the 5-hour block cell (b:20% r:2h13m) before session duration; new tokenSpeed cell (tps:238) — median tokens/sec of recent streamed turns from per-message chunk-timestamp spans.
  • New bottom usage & models row: w:39% r:2d14h · fable-5:$74·99% opus-4-8:$0.53·1% — weekly quota, dormant per-model quota (modelQuota auto-lights when Anthropic populates seven_day_<model> buckets or extra stdin rate_limits keys), and per-model spend (transcript + subagent spend merged from the events DB via events.agentModelCosts).
  • Task cell reads BOTH ~/.claude/projects/<encoded>/session-context.md and a repo-root copy (fresher wins) — fixes a 3-month-stale task pin; shared parser tolerates annotated ## Current Task — … headings.
  • Token counts roll to B past a billion (t2.4B); contextPct clamps to 90% of the real window so 200k-window models can reach 100%.

Misc

  • CI workflow added (node 20/22, npm test).
  • Docs: tool-archive path corrected to ~/.claude/state/; README fields/presets updated.

Test plan

  • 235/235 node:test suite (16 new tests: pricing families, Sonnet 5 date flip, per-turn pricing, plain-transcript lister, pairing map, reconcile backfill, reprice migration, Retry-After parsing, error-cache carry, backoff, model quotas, per-model costs, token speed)
  • Live verification: DB migration + 403-row backfill on real data; statusline rendered against the live 375MB CSM session transcript (2.6s cold cache build, 0.13s warm) and this session
  • Raw API probes confirming endpoint behavior (200 schema captured; second request within 1 min → 429 Retry-After 3600)
  • Watch first SessionStart in CSM for subagent usage backfilled log line
  • Confirm modelQuota stays dormant (renders nothing) until per-model buckets are non-null

…age-API outage resilience

Full-stack audit (2026-07-17) found the model-upgrade wave had outrun SI's
tables and the background-agent default had blinded the tracker. Fixes:

Pricing (lib/cost-estimation.js):
- Add claude-fable / claude-mythos families ($10 in / $50 out, 5m cache
  write $12.50, cache read $1) — they previously fell through to the
  legacy-Opus default.
- Reprice Opus 4.5+ to $5/$25 (rows were costed 3x high); Opus 4.1 and the
  dated Opus 4 base id stay on $15/$75 via longer prefix pins.
- Sonnet 5 introductory $2/$10 with a dated flip to $3/$15 on 2026-09-01
  (knownPriceForModel(model, at)).
- totalsFromTranscript now prices each turn by its own message.model; the
  flat prices arg only covers unknown models. Cost caches bumped v2→v3 so
  flat-priced accumulations don't blend in.
- DB migration (user_version 1→2) recomputes agent_invocations.cost_usd
  from stored tokens: live opus-4-8 total corrected $1,122.74 → $374.25.

Subagent usage backfill (events.reconcileSubagentUsage, wired in
si-bootstrap):
- 54% of agent rows had NULL model/tokens/cost — Agent/Task calls run in
  the background by default now, so PostToolUse fires before the subagent
  transcript flushes usage. Reconcile completed transcripts at
  SessionStart.
- Claude Code 2.1.x transcripts no longer carry parentToolUseId; pair
  agentId ↔ tool_use_id from the parent transcript instead (launch-ack
  tool_result text + task-notification markers) via mapAgentToolUseIds.
- Live run backfilled 403 blind rows; the rest have evicted transcripts
  and age out of the 45-day window.

Usage-API outage resilience (usage-refresh/usage-api/statusline):
- Honor 429 Retry-After (observed 3111s) instead of re-fetching every
  ~30s through an outage; 5-minute default backoff for other errors.
- Error caches carry the last good values + goodAt forward; statusline
  renders them with a ~ stale marker instead of blanking the b:/w: cells.

Statusline:
- contextCap recognizes fable/mythos as 1M-window models.
- contextPct denominator clamps to min(zones.red, 90% of the model window)
  so 200k-window models can actually reach 100%.

Docs: tool-archive path corrected to ~/.claude/state/ (was ${TMPDIR}).
CI: add .github/workflows/test.yml (node 20/22, npm test — 226 tests).
The task cell only ever read ~/.claude/projects/<encoded>/session-context.md.
Projects that hand-maintain a repo-root session-context.md (long-lived
orchestrator sessions like CSM) left the projects-dir copy frozen — its
user-authored content blocks autofill by design — so the statusline showed
a 3-month-old task, permanently marked (stale).

- loadCurrentTask now reads BOTH copies (bounded ancestor walk from cwd
  finds the repo-root file) and renders the fresher one.
- session-context parser: section lookup tolerates annotated headings
  ("## Current Task — #1695 Phase 2 …"), which previously parsed as a
  missing section for every consumer (statusline, task-change, pre-compact).
- strip markdown bold markers from the rendered task text.
…stdin

The b:/w: usage cells polled api.anthropic.com/api/oauth/usage — an
endpoint that aggressively 429s pollers (anthropic/claude-code#31637,
closed not-planned). Today it rate-limited all day, Retry-After re-arming
to 3600s, and the cells stayed blank since morning.

Claude Code ≥2.1.90 already passes the quota in the statusline stdin JSON:
rate_limits.five_hour/.seven_day {used_percentage, resets_at}. Prefer that
— fresh every redraw, zero API requests. The oauth poller (with the new
Retry-After backoff + last-good carry) remains as fallback for older
builds only.

Also prefer stdin context_window.context_window_size over the model-name
regex in contextCap — the session's real window beats any name guess.
New bottom line on the verbose/verbose-cache presets showing which models
this session's dollars went to, e.g. `fable-5:64% opus-4-8:36%`. Cost
share (not token share) via the per-model pricing tables, top 4 models.
There is no per-model QUOTA to show — stdin rate_limits only carries the
two account-wide windows — so share-of-session-spend is the honest
per-model percentage.

Both transcript accumulators grow per-model cost buckets (byModel),
cache v3 → v4 (bump must stay in lockstep between lib and statusline —
they share the cache file).
…t modelQuota field

"Used / available for models" — built from what actually exists:

- blockUsage/weekUsage render used/available (`b:7/93%`, `w:38/62%`)
  instead of used-only.
- modelSplit adds dollars used per model: `fable-5:$3.55·76%`.
- New modelQuota field for per-model QUOTA used/available. Dormant today:
  verified via a raw 200 that the oauth payload's seven_day_opus/_sonnet/
  _cowork buckets exist but are null on plans without per-model limits,
  and stdin rate_limits only carries the two account windows. The field
  auto-lights from either source (extra stdin rate_limits keys, or
  populated seven_day_<model> buckets captured by the usage worker) the
  day Anthropic ships per-model limits.

Endpoint reality check that shaped this: one follow-up probe within a
minute of a 200 re-tripped the rate limit with Retry-After 3600 — the
endpoint tolerates roughly one request per hour, cementing stdin-first
sourcing as the only viable live display.
blockUsage lived on line 2 and weekUsage on line 4; user feedback: all
quota/usage belongs together at the bottom. Line 5 is now the usage &
models row — `b:7/93% · w:38/62% · [modelQuota] · fable-5:$3.55·76% …` —
and lines 2/4 slim down to activity and identity respectively. Same
change on verbose-cache.
…ever shows one model

The bottom-line model split read only the parent session transcript, and
subagents (Explore/reviewers/workflow agents) bill in their own transcript
files — so a Fable session with Opus/Haiku agents rendered `fable-5:100%`.
Merge per-model agent spend from agent_invocations (events DB) into the
split, keyed by sid. New events.agentModelCosts(sid) helper; lazy sqlite
load in the statusline (adds ~10ms, cached per redraw process; degrades
silently without better-sqlite3).

Known lag: background agents record blind until the next SessionStart
reconcile fills their usage, so the newest agent runs can be briefly
absent from the split.
- New tokenSpeed field, last block of line 2: `tps:238`. Median output
  speed of the last few streamed turns from lib/token-speed.js — the
  transcript writes one row per stream chunk sharing the message id, so
  the per-message timestamp span is pure generation wall-clock and
  final output_tokens / span is an honest turn speed. Session-average
  fallback (total output / total_api_duration_ms from stdin) when the
  tail has no streamed turn.
- fmtTokens/fmtTokensTight roll to B past a billion: `t2.4B` instead of
  `t2422M` — 10-day sessions really do get there.
…5/85%`

The used/available slash form kept being read as a fraction ("15 of
85?"). The remainder now carries its own label so the cell needs no
legend: used% keeps the zone colouring, `free:` and `r:` render dim.
…line 2

Per user feedback: `free:` removed from both quota cells (b:15% / w:39%),
and the 5-hour block's reset countdown is now its own blockTimer cell
(`blk:2h29m`) at the head of line 2, before session duration — it's a
"when do I get budget back" signal that belongs with the live-activity
row, not the bottom usage row. Weekly keeps its r: countdown on line 5.
…ckTimer

Misread of user feedback: they wanted the blockUsage cell (b:20% with its
reset countdown) moved to line 2 before session duration, not a separate
timer cell. blockUsage leads line 2 with r: restored; blockTimer removed;
line 5 keeps weekly + per-model spend.
…on estimate

Requested format `fable-5:sc$78·sp99%·wr[weekly remaining]` per model.
True per-model weekly quota doesn't exist (seven_day_<model> buckets are
null on plans without per-model limits), so wr is an attribution
ESTIMATE, marked `~`: wr(model) = 100 − account_weekly_used% × model's
share of this week's tracked spend.

Weekly spend tracking: new session_model_costs table (sid, model,
cost_usd, t) upserted by the statusline each redraw from the transcript
byModel buckets — the only surviving record of parent-session per-model
splits once transcripts age out. weeklyModelSpend() unions it with
agent_invocations over a 7-day window.
@dayakarreddyn
dayakarreddyn merged commit 42edc67 into main Jul 17, 2026
0 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant