Skip to content

🧾 parse/cache: capture effort + usage.iterations from JSONL β€” fallback-aware per-model token/cost accountingΒ #456

Description

@martinciu

🧭 Context

A scan of local transcripts (2026-07-20) found two JSONL envelope additions ccpulse doesn't parse:

  • 🎚️ effort β€” top-level on assistant lines since Claude Code 2.1.212 (first seen 2026-07-17). Records the effort level the request ran at (e.g. "xhigh").
  • πŸ” message.usage.iterations β€” on every assistant line since Claude Code 2.1.119 (April 2026). A list of per-attempt usage objects, each with its own model, full token breakdown (cache_creation included), and type ("message" | "fallback_message").

Normally iterations is a single entry duplicating top-level usage. But on refusal-fallback turns it has one entry per attempt, and top-level usage covers only the serving attempt. Real example from local history:

"model": "claude-opus-4-8",            // serving model on the line
"usage": { "input_tokens": 2, "output_tokens": 4594, ... ,
  "iterations": [
    { "model": "claude-fable-5",  "output_tokens": 434,  "cache_read_input_tokens": 686578, "type": "message" },
    { "model": "claude-opus-4-8", "output_tokens": 4594, "cache_read_input_tokens": 598887, "type": "fallback_message" }
  ]}

ccpulse attributes this turn entirely to Opus and silently drops the refused Fable attempt (434 output tokens + 686K cache reads β€” billed, since it refused mid-stream). 13 multi-attempt turns exist in local history today. This is the same per-model attribution gap as #455 approaches from the API side: the Fable weekly limit is consumed by attempts ccpulse can't see.

Related markers checked and deliberately skipped: fallbackModel/originalModel on system lines (redundant with iterations), usage.speed (uniformly "standard"; only matters if fast mode is used), usage.server_tool_use (zero across all history), service_tier/inference_geo (constant).

πŸ“¦ Scope

  1. pkg/parse: extend the envelope with top-level effort and message.usage.iterations (per-attempt model, token fields, cache_creation, type). Null-tolerant: both absent on pre-2.1.119 / pre-2.1.212 lines.
  2. pkg/cache: store effort as a column on messages; store per-attempt usage in a way cost/token aggregation can consume.
  3. Accounting: token/cost aggregation (OutputTokenBuckets, cost view, status) becomes iterations-aware β€” sum billed attempts at each attempt's own model pricing instead of trusting top-level usage + line-level model.
  4. Schema version bump β†’ auto-rebuild. 🎁 Unlike πŸ—„οΈ anthro/cache: parse + persist the usage API limits array (per-model weekly limits)Β #455's usage_samples, this data source is the JSONL itself β€” a rebuild retroactively backfills effort + iterations for every transcript still on disk.

πŸŒ€ Open design question (brainstorm before implementing)

How to represent a turn that consumed two models' tokens in a one-row-per-line schema:

  • (a) child table message_attempts (message_id, attempt_idx, model, token columns, is_fallback) β€” keyed by message.id so it dedupes naturally across multi-content-block lines sharing an id (see πŸ› cache: dedupe usage by message.id β€” multi-content-block turns over-count tokens/cost up to ~100Γ— (Opus 4.8)Β #374);
  • (b) explode into synthetic per-model message rows β€” breaks message.id dedupe semantics; probably a dead end;
  • (c) single-entry fast path β€” when iterations has one entry it duplicates top-level usage; decide whether to store child rows always (uniform reads) or only for multi-attempt turns (tiny table, special-cased reads).

⚠️ Interaction with #374 dedupe: multiple JSONL lines of one assistant turn share message.id and repeat the same usage object β€” iterations must be counted once per message.id, not once per line.

βœ… Acceptance

  • Assistant lines with effort land it in the DB; pre-2.1.212 lines store NULL.
  • Multi-attempt turns contribute each billed attempt's tokens/cost under that attempt's model; single-attempt turns produce results identical to today.
  • Rebuild backfills historic effort/iterations from existing JSONL.
  • Cost view / status --json totals change only on the multi-attempt turns (13 locally) β€” verifiable before/after.

Related: #455 (API-side per-model limits storage).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestgoPull requests that update go codeimportance: mediumMeaningful improvement; not blockingsize: lLarge β€” half a day or more

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions