Commit graph

18 commits

Author SHA1 Message Date
Aditya Vikram Singh
f816c5e3de fix(report): bill remaining displayed output per call
Compare, export, report JSON, overview Top-model/By-tool/day, and
menubar localModelSavings still summed raw output. Session totals
stay defined on aggregate-only and stub calls. Upgrade verifier
expects daily-cache.v28.json.
2026-08-23 18:18:57 +05:30
iamtoruk
fa89015947 fix(models): price claude-haiku-4.5 copilot session-store rows (#1093)
The Copilot session-store.db writes the model as claude-haiku-4.5
(tier-first, dot), but no pricing alias existed for that raw id, so
calculateCost returned $0 while GitHub billed the real Anthropic rate
(kelchm's #946 validation: $0.0063536 for a sampled request). Aliases
it to the existing, correctly-priced claude-haiku-4-5 snapshot row --
no new rate invented. Unlike #1090's gpt-5.6-codex case, this id has
no prefix-fallback self-heal, so the daily cache bumps v26 -> v27 to
force re-derivation of already-finalized days.

Drafted with cline-pass/deepseek-v4-pro via local gateway.
2026-08-22 11:41:08 -07:00
iamtoruk
c30ebecff4 Merge origin/main into feat/copilot-session-store
Resolves the conflicts #946 accumulated while it was in validation. Eight
files conflicted; the session-store accounting is unchanged.

src/daily-cache.ts — version collision. This PR minted 25 when main was at
24; #1056 (`codex-auto-review` pricing) then spent 25 on main. The bump moves
to 26/MIN 26 and daily-cache.v26.json, with main's full comment ladder kept as
the foundation and this PR's paragraph rewritten to name 26 and record the
collision. PENDING_REDERIVE_PROVIDERS and the B1 migration semantics from
b6481c19 carry over intact, retargeted at 26.

src/models.ts, src/parser.ts, src/audit-report.ts, src/models-report.ts —
#1075/#1078 replaced the per-site "reasoning is already inside output" tests
with billableOutputTokens() and REASONING_INCLUDED_IN_OUTPUT. This PR had
added copilot to that case at three sites independently. Union: all three
sites take main's helper call verbatim, and copilot joins claude and codex in
the set — same accounting this PR shipped, now through main's single source of
truth. It also reaches parser.ts activeGeneratedTokens (a fourth site, from
#1079), which is the same correction: a copilot supplementary call carries
reasoning with output 0, so counting it as generated repeats the per-turn
output. The audit legend already said so on this side.

src/providers/copilot.ts — comment-only. #1054's lastEventTimestamp-first
shutdown fallback was derived from this branch, so the code was already
identical on both sides: the `shutdownTimestamp` expression and the
`copilot:<sid>:shutdown:<model>:<n>` key are byte-for-byte main's. Both
rationales are kept (leg-collapse on date, and residual anchoring).

src/session-cache.ts — PROVIDER_PARSE_VERSIONS.copilot takes this PR's
`-session-store-v3` suffix; main's #1051 note about why a fingerprint change
is expensive is kept above it. Codex keeps main's #1092 suffix chain untouched.

src/main.ts — #1067 deleted the unreachable live dailyMap fallback that this
PR had taught behavioral weight. Main's deletion wins; the now-unused
isBehavioralTurn import goes with it.

tests/parser.test.ts — import union.

Also: scripts/upgrade-path/run.mjs NEW_DAILY_CACHE -> daily-cache.v26.json,
CODEBURN_COPILOT_SESSION_STORE_DB added to the #1064 env-isolation CLEARED
list, and the CHANGELOG entry's stale "v21" corrected to v26.

Verified: tsc clean; 3132 tests pass across 223 files; test:locks 26/26;
verify:upgrade PASSED, re-deriving daily-cache.v26.json and holding durable
copilot history across the bump. #1054's regression ("keeps three stampless
shutdown legs as :n keys with lastEventTimestamp") passes on the merged tree.
Real-corpus A/B against origin/main over 2026-07-01..2026-08-22: codex, grok,
kimicode and opencode byte-identical in export, audit and models; claude drifts
only monotonically with run order (a live session writing transcripts, confirmed
by interleaving four runs). This machine has no copilot data, so the copilot
recovery semantics rest on the suites and the upgrade-path corpus.
2026-08-22 09:36:51 -07:00
iamtoruk
12c8cb7790 fix(copilot): re-derive migrated copilot day slices, count the compaction row's output
Two defects from @kelchm's two-machine validation of #946.

1. The daily-cache migration carried the copilot slice of every existing day
   entry verbatim instead of re-deriving it, so `overview` on a migrated cache
   kept serving pre-store numbers (2,980,804 tokens for a day whose fresh
   derivation is 74,811,412; 21,608 calls vs 16,326) while export/models/audit
   served the corrected ones off the same cache.

   Root cause is the partial-survival guard (e9d922ca), not the migration: it
   keeps a settled baseline slice whenever the fresh derivation reports FEWER
   calls, reading that as aged-out sources. This change's whole point is that
   copilot's supplementary accounting calls stop counting as api calls, so
   every store-era day shrinks in calls and got pinned to its pre-store value.
   The guard's own TRADE-OFF note predicted exactly this case.

   A migration from an older cache version now grants copilot ONE
   guarded-shrink-exempt re-derivation (`pendingRederive`), spent by the first
   COMPLETE parse. It fires only where that parse actually produced a slice for
   the (day, provider), so a day whose sources are gone still carries forward
   whole - never-lose is unchanged in both directions - and no other provider
   is exempt.

   The daily cache moves 21 -> 25: 21-24 landed on main during validation, and
   only a number the validators' own daily-cache.v21.json cannot claim gets
   their stale carried slices re-derived.

2. The `initiator='compaction'` store row was ingested with outputTokens 0. Its
   output has no assistant.message anywhere in events.jsonl, so that row is the
   only place those tokens exist - dropping them was the sole token discrepancy
   (-3,085) across a 30-session store-matched comparison. It is now read (on the
   same optional-select rung as `initiator`, which is what identifies the row),
   counted and priced; every other row keeps output 0 because a per-turn call
   owns it. The copilot parse version moves to session-store-v3 so cached v2
   rows re-parse; the dedup key is deliberately unchanged.

Settle window stays at 24 h - the three-machine evidence for tightening it is
recorded next to the constant, but shortening it is a product call.
2026-08-22 02:26:35 -07:00
Aditya Vikram Singh
090499ef7e merge: rebase #1056 onto main with Codex v14 / daily v25
Main already spent Codex results v13 (#1079) and daily v24 (#1090).
Keep all three parse-version tokens (codex-pricing-v1 +
codex-tps-v1 + activity-price-v1) so a take-ours merge cannot
drop the pricing or throughput invalidation. Do not Extra High again.
2026-08-22 05:26:00 +05:30
iamtoruk
98348d973b fix(models): price gpt-5.6-codex and gpt-5.6-codex-max
Neither id is in LiteLLM yet, so litellm-snapshot.json had no row for
them (285 sessions / 5,446 calls priced at $0 on one real corpus,
flagged during #1075 verification). Add explicit snapshot rows sourced
from the pattern every prior Codex-suffixed id LiteLLM does carry
follows: it bills identically to its bare-model sibling of the same
generation (gpt-5-codex == gpt-5, gpt-5.1-codex == gpt-5.1-codex-max ==
gpt-5.1, gpt-5.2-codex == gpt-5.2, gpt-5.3-codex == gpt-5.3, verified
against the live model_prices_and_context_window.json) - both new ids
get the exact gpt-5.6 tuple rather than an invented rate.

getModelCosts already resolved both ids to the correct rate through
the `gpt-5.6` prefix fallback before this, so a fresh parse or a warm
session-cache read (codeburn models included) was already pricing
these calls correctly. The daily cache is the one place that does not
self-heal: it has no per-provider invalidation, so a day finalized
before a `gpt-5.6` snapshot row existed at all keeps that $0 forever.
Raising MIN_SUPPORTED_VERSION (v23 -> v24) forces the one-time
re-derivation, a lossless no-op for days already correct - the
mechanism itself is generic and already covered by
daily-cache-version-rederivation.test.ts. #1056 also claims 24 on its
own branch; whichever lands second takes the next number.

Closes #1077
2026-08-21 14:53:00 -07:00
Aditya Vikram Singh
33d344abc3 merge: rebase #1056 onto main with Codex v12 / daily v24
Main already spent Codex results v11 and daily v23 on #1075, and
result caches are now version-suffixed. Keep both parse-version
tokens (codex-pricing-v1 + activity-price-v1) so a take-ours merge
cannot drop the pricing invalidation. Do not Extra High again.
2026-08-22 02:35:51 +05:30
iamtoruk
fda7e8024d fix(codex): stop double-billing reasoning output, price cache writes at the explicit rate only
Reasoning tokens are a subset of output_tokens for OpenAI models, not an
extra bucket: on a 1,396-rollout corpus all 134,316 token_count events
carrying a total satisfy input + output == total. CodeBurn added
reasoning_output_tokens on top when pricing a codex call, in the
cache-rehydration re-price, and in the models/audit display sums. That
overstated codex cost by $166.03 (3.5%) and displayed output tokens by
34.6% on that corpus. Both cost sites and the display sums now go through
one shared billableOutputTokens() so a cold parse and a warm read cannot
drift apart.

cache_write_input_tokens (codex PR #33454) was never read and
cacheCreationInputTokens was hardcoded to 0. It is now carved out of the
uncached-input bucket and clamped to it, but routed to the cache-write
bucket ONLY when the pricing source publishes an explicit cache-write rate.
buildCosts fabricates 1.25x input when a source omits one, which is correct
for Anthropic and would have invented a surcharge OpenAI never charged on
gpt-5.5 / 5.4 / 5.3-codex / gpt-5. ModelCosts now carries
cacheWriteCostIsExplicit so that distinction survives getModelCosts.

A cost change invalidates persisted output: codex-results.json v10 -> v11
(stores costUSD verbatim), the codex parse version moves (the token-bucket
change does not self-heal on read), and the daily cache goes 20 -> 23 (21 is
claimed by the #946 landing branch and 22 by PR #1056). The upgrade-path
corpus asserts codex tokens and calls exactly and reports the repricing.

Closes #1075
2026-08-21 11:50:23 -07:00
Aditya Vikram Singh
61e76b7989 fix(models): price Codex auto-review as GPT-5.5 and take daily v22
Maintainer review on #1056: pin the activity id to the recommended
review model (existing bundled row, not an invented rate), resolve
the same alias for credits, and take daily-cache 22 so this PR does
not collide with #946 claiming v21.
2026-08-21 15:56:25 +05:30
iamtoruk
f77687a8b3 Merge origin/main, and act on vidoluco's real-Copilot findings
Merge resolution
- daily-cache: main shipped v20 with #1040, so 21 is now the first FREE number
  rather than one dodging an unmerged head. Both version notes kept, ours on
  top. The re-derivation seed stays one below current (20), now the shipped
  predecessor rather than a draft.
- upgrade-path: expected daily cache filename stays v21.

The rollup is cumulative across resume legs — already handled, now pinned
Measured on a real 3-leg session (CLI 1.0.80): every counter in a leg includes
the legs before it, and the last leg of a complete session equals its store-row
total to the nano. The parser has always emitted `cumulative - previous
cumulative` per model, so the interval arithmetic downstream already consumes
per-leg claims; it was the PR body and the provider doc that described the raw
journal as per-leg and misled the reader. Doc corrected, and (c3) pins it end
to end: a complete 3-leg session serves 900 (its last leg / store total), not
1,400 (the sum of its legs); an uncovered one serves 900 exactly once; and a
leg reporting LESS than its predecessor is taken as a fresh epoch rather than
clamped to a negative delta, so an older per-leg CLI never loses a leg.

initiator = 'compaction' replaces the timestamp heuristic where it exists
The summarization request does write its own assistant_usage_events row, and
newer stores label it. The label is now read — schema-adaptively, and the
enrichment select is a graduated chain so a store carrying the billing columns
but not `initiator` keeps its billing metadata instead of falling all the way
back to the base select. Two uses: the row is subtracted from the leg it
belongs to even though it commits before the compaction stamp, and it is kept
out of per-turn pairing since it has no assistant.message to pair with.
Optional twice over — absent on older stores, NULL on 1,504 of 2,509 rows on a
real one — so (c5) pins the labelled path at 350, the identical UNLABELLED
fixture at 400 (the documented one-request-per-compaction over-serve, which no
timestamp rule can close: the request that triggered the compaction completes
immediately before it too), and a compaction row never stealing a pairing
partner.

Attribution invariant, standing guard for the 1.8x report
(c4) runs `codeburn audit`'s own two numbers — attributed vs recomputed —
over all four combinations of the three representations a session can be
written in, at the magnitudes of a real reported day (gpt-5.6-terra, 146 rows,
input 17,792 / cache write 501,395 / cache read 12,097,364 / output 63,344 /
reasoning 24,831, billed $4.47). Every shape reconciles at 1.000, and the
covered case lands on $4.4687 = what the tokens price at = what GitHub billed,
with output 63,344 rather than the pre-fix 88,175 that re-priced reasoning on
top of itself.

upgrade-path: a day KEY is not the unit of never-lose
A parse change that re-dates a call to its true day legitimately empties one
day and fills its neighbour, token for token — observed on a real cache where
2026-08-08's single call moved to 08-07 exactly. The aging step now compares a
+/-1 day WINDOW rather than the day alone, so only a window that shrinks is a
loss, and an emptied day key is reported rather than failed.

RECONCILE_SETTLE_MS keeps 24h, with the measurement that argues against it
recorded at the constant and in the provider doc: across 91 real sessions zero
rows landed after shutdown (median -0.1s, max -0.0s). One machine, one CLI
version — the number to beat is seconds, not hours, once a second machine
agrees.
2026-08-21 02:14:14 -07:00
Aditya Vikram Singh
f736569435 fix(models): reprice cached Codex auto-review and drop unproved sibling
Extra High MERGE AFTER FIX on #1056: bump Codex/session/daily cache so the 659M-token corpus reprices; do not invent codex-code-review.
2026-08-20 11:08:38 +05:30
iamtoruk
9ebd509c44 fix(sync,cache,ci): review A — forward-only sync, a guard that bites, call-level union
F1 — the upgrade-path guard was measuring the wrong layer. providerDetails
come from the menubar payload, and on a version bump adoptOlderDailyCaches
carries the superseded daily file forward as the baseline for exactly the days
this scenario creates, so a session-cache loss was mostly masked: reverting the
carry-forward showed a 10% dip where the real loss is 50%. It now sums
export.json's per-call copilot records (straight out of the parse), deletes the
baseline daily cache before the upgrade run so nothing else can answer, and
asserts EQUALITY rather than "did not shrink" — the failure mode on the other
side of the fix is a double. Proven: reverting the carry-forward now reports
80 calls -> 40, LOST history, and fails. (The double direction is asserted but
not demonstrated; a faithful re-keying simulation is collapsed by serve-time
dedup before it can be counted, which is noted at the step.)

F2 — sync was forward-only in the wrong direction. A receiver that took
`copilot:<sid>:shutdown:<model>:<n>` rollup spans from 0.9.20 keeps them
forever (append-once, no retraction for a usage span) and would now also
receive `copilot-store:` rows for the same sessions: permanent doubling on
every synced receiver, on the first push after upgrade. Sessions whose rollup
is already in the ledger are now FROZEN — their rows and residuals never go
out, and the receiver keeps the older, lossier number. A bounded under-count
in place of an unbounded over-count. Per-turn output spans are untouched
either way (the rollup never held output, reconciliation never touches them),
and sessions new to sync take the per-row path in full. `frozen` is reported
by --dry-run and the nothing-to-push line; docs/sync/README.md gains an FAQ
pointing at `codeburn sync reset --confirm` with the caveat that the receiver
must be cleared too.

F3 — docs/providers/copilot.md gains a Sharp edges section saying plainly that
a day sealed on a short store snapshot is a permanent UNDER-REPORT the
watermark advances past, not a stall, and that both realizable causes are now
closed (read ordering, compaction anchor).

F4 — the copilot dedup-key shapes are pinned by reading them back out of the
source, minted keys and discriminator prefixes alike, with the reason: the
durable union recognises history by key, so a changed prefix is a
CACHE_VERSION bump, not a parse-version bump.

F5 — the carry-forward comment now states its price: a bump no longer
re-derives a call it carried forward, so cost-shaped fixes land (cost is
recomputed from cached usage) but metadata and day-attribution fixes reach
only newly parsed calls.

F6 — the durable union filters at CALL level. A turn holding one cached call
beside a new one used to be dropped whole; nothing enforced the one-call-
per-turn assumption that made that safe.

F7 — the retain-source fence no longer skips a store it cannot fingerprint.
Present-but-unreadable defers, matching the classification path; only a
genuinely absent one stays a skip.
2026-08-19 13:12:18 -07:00
iamtoruk
aeaa11f73f ci(upgrade-path): guard durable history across a bump on an extant source
The corpus had no durable SQLite provider, so the one never-lose direction it
could not reach was the one this change breaks: a source that keeps its file
while the provider prunes rows out of it. gen-corpus now writes copilot's OTel
span store at its real default path (80 spans over 40 days), and the run caches
it with the published CLI, deletes half the spans from a DB that stays present,
valid and populated, then upgrades through the parse-version bump.

Copilot cost must not shrink across that, and must still not shrink on the
warm run that reads the rewritten cache — a carry-forward that only survived
in memory fails the second check. Verified to bite: with the carry-forward
reverted the step reports LOST $0.361746 (10.0%) and fails.

Also: the expected daily-cache filename moves to v21.
2026-08-19 12:46:01 -07:00
iamtoruk
f060a2d806 Merge origin/main into feat/copilot-session-store-landing
Resolved against main's newer invariants:

- daily-cache: DAILY_CACHE_VERSION/MIN_SUPPORTED -> 21. Main shipped 19
  (#1015 grok); 18 was burned by this branch's earlier public head and 20 is
  claimed by the unmerged #1040, and isMigratableCache would adopt either as
  finalized without re-deriving. Header keeps main's v19 note and adds v21's.
- session-cache: PROVIDER_PARSE_VERSIONS keeps main's grok/dsh entries and
  appends this branch's `-session-store-v2` to main's copilot value.
- parser: durable age-out takes main's orphan-only rule (#992).
  retainWhilePresent is now redundant under it rather than load-bearing.
- audit-report: both imports (isBehavioralCall + sanitizeModelForDisplay).
- tests/parser (e), (l), (sc): re-pinned to the orphan-only age-out. A
  still-discovered >90d copilot events.jsonl now keeps serving its per-turn
  output alongside the store rows; the (sc) age-out subject is now an orphan.
- daily-cache-version-rederivation: seed 20 so the adjacent-draft-version
  case is what the test pins.
- scripts/upgrade-path: expected daily cache filename -> v21.
2026-08-19 12:29:33 -07:00
iamtoruk
52ceceb211 cache: re-derive the daily rollup for the codex session_meta model fix
The codex parse version and CODEX_CACHE_VERSION bumps in #1040 make codex
sessions re-parse, but the daily cache has no per-provider invalidation, so
every day already finalized keeps its old per-model rows - and usage-aggregator
serves every day before today from that cache, with ten-year retention. Raise
DAILY_CACHE_VERSION and MIN_SUPPORTED_VERSION to 20 so history re-derives once
off the warm session cache.

The re-derivation test now seeds v19, the last shipped version, so it models the
real 19 -> 20 path, and the upgrade-path check expects daily-cache.v20.json.

Measured on a real 110-day cache: no day lost value, none disappeared, 100 came
back identical, and 9 grok days rose by $19.80 in total from rollups an earlier
parse change had left stale. Every codex model row was unchanged - that corpus
predates the provenance field the fix corrects.
2026-08-19 11:28:40 -07:00
iamtoruk
34726fb63c ci(upgrade-path): reference #1032 in the shard-republish note 2026-08-18 12:20:34 -07:00
iamtoruk
b2e59c3a2f ci: fail the upgrade path when a partially aged-out day loses its slice
Claude Code deletes its transcripts after ~30 days, so between one run and
the next a day can go from fully sourced to PARTIALLY sourced. On such a
day the daily cache re-derives a smaller slice from the surviving files
and that slice REPLACES the baseline one rather than being unioned with
it, so the aged-out portion is lost. A day that aged out completely is
carried forward correctly, which is what makes this a hole in never-lose
rather than a missing feature.

The check ages the corpus the way retention does: two days keep a single
anchoring transcript each, a third loses every one. It then compares the
(date, provider) slices in daily-cache.v19.json against the ones the
0.9.20 baseline recorded in daily-cache.v17.json, requiring the partial
days never to shrink and the fully sourceless day to come back exactly.

This is a hard failure, not a note. It is expected to be red until the
fix lands: on the generated corpus the two partial days currently lose
76.6% and 56.8% of their cost, while the fully sourceless control day
returns to the cent.

It runs last and on its own cache dir, so mutating the corpus cannot
disturb the payload parity comparison. The shard-republication defect
found earlier stays a note, now naming its pending follow-up issue.
2026-08-18 12:19:45 -07:00
iamtoruk
74d718fe03 ci: verify the 0.9.20 upgrade path on every platform
Everyone upgrading from the last published CLI crosses the session-cache
v7 -> v9 re-layout (#1005/#1007) and the daily-cache v17 -> v19
re-derivation (#1015) on their first run. That path was covered by unit
tests on one platform against caches the tests wrote themselves.

`npm run verify:upgrade` (and the matching matrix job over
{ubuntu, windows, macos} x node {22.13.0, 22}) instead installs the real
codeburn@0.9.20 into an isolated global prefix, points it at a generated
seven-provider corpus in a HOME whose path contains a space, and lets it
write a genuine session-cache.v7.json + daily-cache.v17.json. This build
then runs against that same cache dir, installed the same way, so
dist/parse-worker.js has to resolve from an entry point outside the
checkout.

It asserts the v7 file is retired, the v9 envelope and shards publish,
the daily history does not shrink, and per-provider calls/tokens/cost
match the baseline exactly for claude, codex, gemini, kiro and cursor.
grok is reported rather than asserted (its accounting changed in #1015)
and dsh is required to be new. It also smoke-tests serve --stdio against
the one-shot payloads, pins CODEBURN_PARSE_WORKERS to 0 and 3 and
requires identical shards and payloads either way, and checks that a
second run re-parses nothing.
2026-08-18 12:14:55 -07:00