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-DDform. 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 itsreportingPeriod, not its day count, which grows through the month.--period allreports 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.