Part of #4.
What is there today
EngineResult.costCents (app/Uplink/engine.ts#L33) is filled from Claude's total_cost_usd and
turns from num_turns. Codex's turn.completed carries only
usage: {input_tokens, cached_input_tokens, cache_write_input_tokens, output_tokens, reasoning_output_tokens}
and no cost field at all.
Both fields are already nullable, so nothing crashes. The damage is in the reporting:
app/Uplink/dashboard.ts#L52sumsrow.cost_cents ?? 0intototals.spendand renders it as one dollar figure. A Mac running mostly Codex shows a total that is quietly short rather than absent, which is worse than showing nothing.app/Commands/Uplink.ts#L151prints, N¢ at API ratesonly when a cost exists, so a Codex run silently loses its line.- The README's claim that runs are recorded with "cost in integer cents, at API rates" becomes true of only some runs.
Proposal
Decide the shape before the engine column lands, because the column is what makes the mixed history visible.
- Record what each engine actually reports. Add token counts to the run record alongside
cost_cents, so a Codex run stores something real instead of a null. - Make the dashboard total say what it covers: "spend across N Claude runs" rather than one figure over a mixed history, and show Codex runs with their token counts.
- Do not price Codex tokens into cents from a hardcoded table. A price table in the app goes stale silently and the figure would be presented with the same confidence as Claude's reported cost, which is measured rather than estimated.
Requirements
- A null cost must never read as zero cost in any total.
- The dashboard and the
buddy uplink:*output agree. Two different answers for the same history is worse than either. - Existing rows keep their meaning. No backfill invents numbers.
Acceptance
- A mixed history of Claude and Codex runs shows a total that names what it covers.
- A Codex run's row shows its tokens.
- No total sums a null as zero.