docs/cost-history.md

Cost history and daily models

The provider popup shows Today and the selected period side by side. History, period models, and projects expand on demand. Cost errors and pricing notices remain visible when the details are collapsed, including refresh errors beside retained values. A successful refresh clears the error.

Today is the CLI's total for the local date of the scan (updatedAt). A retained snapshot scanned on an earlier date, such as before midnight or before a failed refresh, shows Today as unavailable instead of that day's spend. A snapshot without a usable updatedAt keeps its Today amounts.

Data and display rules

  • Day models come from the selected cost.daily[].modelBreakdowns[] record. Missing day models never fall back to period totals.
  • Display at most six models per day and report truncation. Period models also expose truncation and an explicit empty state.
  • Missing costs and token counts remain unknown. Measured zero stays zero; filling calendar gaps only fills metrics actually observed in the snapshot. Valid calendar dates remain inside the selected window even when duplicate dates, malformed records or the scan budget prevent gap filling. Those fallbacks retain observed rows without inventing zero days. Undated legacy labels keep their bounded sequence behavior. Calendar keys accept only strings in YYYY-MM-DD form. Object and array date fields cannot extend All history or interrupt healthy provider snapshots. Quota-week sums mark trailing days after a stale snapshot, the last scanned day of a snapshot taken before today, and measured days with excluded incomplete requests as partial; a newer usage refresh does not make those costs known. Recent history rows show both available amounts, including zero, in either metric mode. The selected metric still controls row filtering and bar scaling. The history rows show the newest seven days, and the peak line names the highest of them, the row their bars highlight. The daily average covers the whole range and divides the measured days only, so a range measured as zero averages to zero; a range with no measured day has no average at all.
  • Month to date and All history come from the CLI's cost --period. Period identifiers accept only strings; malformed values use the existing day-range fallback and preserve a bounded legacy label when supplied. A snapshot belongs to the selected range by its reportingPeriod, not its day count, which grows through the month. --period all reports the days since year 1, so the widget sizes that window from the oldest recorded day to the scan day. Dates after a known scan day do not extend that window or lower its daily averages; the daily and model filters already exclude those records. Without a usable scan date, the window keeps spanning the recorded dates. The chart and daily rows keep at most 365 days, while period models and the daily average cover the whole span within the 2,048-record scan budget; the CLI's totals still cover the whole history.
  • The global activity heatmap uses the unfiltered history's calendar dates, preserving unavailable days at either boundary and between measured days. Unavailable days occupy empty cells, so filtering missing amounts never shifts weekday rows. Rows start on the locale's first day of the week, and empty cells after the newest day close its week. Its visible span and week count include those gaps, bounded to the newest 365 calendar days. Legacy non-date labels retain sequence order and leave the rows unlabelled, because only calendar-aligned days give each row one weekday.
  • Model names are strings. Numeric names are not coerced into display labels. Tied model amounts use the label as a stable ordering tiebreaker.
  • Sharing uses a separate token ranking from the full bounded period model aggregation, before the existing six-row cost ranking is truncated. Cheap or unpriced models remain eligible for the top-token list. Its known omitted-row count and incomplete-scan flag survive privacy projection and export.
  • Standard/Fast totals remain blocked on an official CLI contract. No service tier is inferred from model names or amounts.

These rules are implemented in ProviderNormalizer.js, CostPresentation.js, and ProviderCostSection.qml.

Selection rules

Hover previews stay inside the chart. Pointer or keyboard selection pins a day without fetching data. While a day is pinned, the chart readout and the drill-down heading name it, so the latest-day summary under the chart is hidden. A background refresh retains the pinned date even when its chart position changes; a missing or ambiguous date clears the pin. sourceIndex cannot identify a day across different snapshots.

Provider, account, and history range changes clear the pin and expanded details, including when cached data is reused. Account identity follows the account picker's canonical label, including organization-only identities. Metric changes clear the pin but preserve explicitly expanded period details. Period efficiency stays in the period view, available through All days; it is not shown as a daily figure. A single measurable day remains selectable to inspect its models.

History, trust, and project totals

costHistoryMetric selects cost or tokens across charts from the same loaded payload; the selected metric also controls bar scaling. Metric and day selection must not add CLI calls. historyCoverageIsEstablished controls the collecting history notice; a missing flag counts as established for legacy payloads.

ProviderNormalizer.normalizeCostTrustMetadata bounds coverage, provenance, and the official incompleteRequestCount. CostPresentation.costTrustSummary qualifies provider and global amounts with one shared notice. Missing legacy metadata stays quiet. Keep pricing coverage separate from history collection coverage and never expose raw provenance in QML.

Official CLI 0.60.5 reports incompleteRequestCount on the cost payload, its totals, each daily entry, and each model breakdown, omitting the key when no request was excluded. The counts describe requests that lacked final usage and are therefore missing from the reported tokens and cost. Treat them as a partial-total marker only: sum them under the coverage bound, never fold them into an amount, and keep the notice semantic so a changed count cannot revive a dismissed warning. Quota-week subtotals carry the bounded daily count so a mixed day makes the affected cost and token sums lower bounds, including in privacy mode. Each daily point and each aggregated model row also carries its own bounded count: a model sums its breakdown counts across the range, including breakdowns without measured amounts, and the provider cost section states the count under the history or model row and beside a selected day's amounts, without changing the amount text. A day whose requests are all incomplete arrives without totalCost and totalTokens, which daily normalization already keeps unknown instead of turning into a measured zero. Privacy mode keeps the count, which carries no identity.

normalizeCostProjects retains bounded names and optional amounts, discarding paths and nested source records. Preserve unknown amounts and duplicate names; keep provider currencies separate. Signal truncation at 128 inspected projects per provider or 128 displayed rows overall. Project rows never contribute to provider or global totals. The usage guide describes the range/metric controls and expandable presentation.

Official CLI 0.56.2 emits Antigravity token-only history through the generic cost envelope. Absent costs stay absent in daily/model normalization. Its established empty snapshot uses zero dollar totals despite costs being unavailable, so normalizeProviderCostTotals masks that provider sentinel until an official availability field exists. Token charts remain usable while cost totals and cost-mode charts stay unavailable. Official CLI 0.62.0 emits the same token-only shape for Muse Code: a synthetic model_completed turn counted exactly into sessionTokens, daily entries, and model breakdowns while every monetary field stayed absent. Cursor cost is rejected by that Linux release; neither case authorizes provider fetching or source parsing in QML. The scoped 0.57.0 probes confirm Cursor is still rejected, and the scoped 0.62.0 probes confirm it again alongside the Muse contract; neither retests established-empty Antigravity history.

Track explicit cost availability, Cursor Linux cost, and service-tier totals in TODO.

Identified-request subtotals

The optional identified Codex section is a separate manual path, documented in the usage guide. Its pure decoder validates the exact requested UTC window, schema, safe integer counts, up to 50,000 records/conflict IDs per ledger, and the shared 4 MiB frontend JSON cap. This ceiling is deliberately stricter than the CLI's 32 MiB ledger transport limit; larger results are unavailable, never silently truncated. Duplicate ledger IDs and unsafe sums are rejected. Only aggregate counts leave the decoder.

Successful local-only output is a ledger. Its subtotal sums input plus output for records outside conflictingIDs. With SSH, the CLI owns cross-source deduplication; the frontend validates the reports and bounds of its emitted subtotal. A single available source must match that source's aggregate. An exit-1 envelope can retain data when a source fails; an exit-0 partial subtotal can exclude requests. Availability and request completeness remain independent. With no available source, the CLI may omit subtotal rather than encode null. This was rechecked with the official 0.75.0 binary in an isolated missing-native-root fixture.

No prices are inferred and no account-wide coverage is claimed. The controller starts only on an explicit local or local + SSH action. Context changes, timeout and destruction retire requests without starting replacements. Ordinary history, provider totals, chart selection, sharing and AI Insights do not consume these results or start their scans.

The command runs under a separate GNU timeout process group: TERM at 110 seconds, KILL after a 5-second grace, before the 120-second controller deadline. A waiting shell keeps that supervisor alive if Plasma disconnects the source early. Missing timeout fails closed. The real-engine lifecycle fixture shortens only the GNU deadlines for stubborn CLI/child processes and verifies that both stop, including after settings changes or controller destruction.

Pinned CLI evidence

The focused 0.75.0 verification confirms that native mixed priced/unpriced history retains its priced USD subtotal and every measured token count. Fully unpriced history omits costs instead of reporting zero. Existing normalization and sharing preserve both shapes, including their incomplete/estimated notice and privacy projection. The separate experimental identified-request local/SSH subtotal has no prices and is not consumed by the widget's generic cost path. A synthetic historical scanner-cache fixture also verifies known-model partial subtotals and their sharing notice. Imported incomplete rows remain unverified.

The scoped implementation used official CodexBar v0.56.8, commit 6ef82690b4a718a21fac42b72a102df07e475509, and its CLICostCommand.swift. The existing daily model fields are modelName, cost, and totalTokens. That scoped implementation added no command or provider-specific source and did not replace the full 0.56.2 parity baseline.

The incomplete-request counts were verified in official Linux 0.60.5 output; the 0.60.5 review records the checksum-verified asset, the isolated probes, and the emitted keys.

The Muse token-only shape was verified in official Linux 0.62.0 output; the 0.62.0 review records the checksum-verified asset (11b88fef999f18cd7fd8b52e52a7fb3eb90e84eaf9fc98a1d4bc5d4170353513), the isolated probes, and the emitted keys.

Linux 0.67.0 adds reportingPeriod (rolling:N, month-to-date, or all) and an English historyLabel to every record. The widget titles rolling:N ranges itself in the user's language and uses a CLI label only for other periods. The 0.67.0 review records the probes.

Linux 0.69.0 still reports its year-1 day count as historyDays for --period all (739890 on 2026-09-29); the 0.69.0 All-history scan fix is internal, so the widget's 365-day chart bound stays. The 0.69.0 review records the probes.

Linux 0.72.0 still reports its year-1 day count as historyDays for --period all (739896 on 2026-10-05, six days later, so the 0.71.x scan efficiency fixes are internal too and the 365-day chart bound stays). The 0.72.0 review records the probes.

The official v0.56.8 Linux x86_64 release archive was checksum-verified with SHA-256 ab98788e12840e5689ae505bf62731e0ea0db1c77e63dceda1589b6e795ac5b8. An isolated probe with two synthetic days and two models ran cost --provider codex --days 7 --format json --json-only --refresh --provider-native-only without network access. It exited 0 with empty stderr. The fixture and recorded output are linked in the historical probe report.

Regression coverage

Normalizer tests and presentation tests cover the data rules. The popup smoke scenarios cover pinned-day refreshes, reordering, missing days, cached account changes, ranges, missing token counts, and incomplete models. The popup-cost-refresh-error scenario covers retained costs through empty, malformed, unsupported, and partial error replies, collapsed/expanded details, privacy, an initial failure without cached costs, and recovery. Cost response tests exercise malformed error messages and bounded normalization, including matching daily and model costs when duplicate dates disable gap filling. All-history cases exclude future dates when sizing short and multi-year windows and their averages, retaining the missing-scan-time fallback and input scan bound. Normalizer cases keep older and future dates outside the period across duplicate, malformed, blocked-day and incomplete scan fallbacks without manufacturing zero values. Controller tests use real CLI processes to check healthy provider updates, same-context retention, command changes, forced refreshes, and timeouts. Run make check for logic and wiring, and make smoke for the real applet with synthetic data. Executed check results belong in the PR.