mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-21 06:24:32 +00:00
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.
This commit is contained in:
parent
952a301877
commit
52ceceb211
4 changed files with 30 additions and 12 deletions
|
|
@ -33,6 +33,7 @@
|
|||
- **The resident `codeburn serve` child.** The first real panel request is also the cache warm-up, so startup never runs an artificial warm-up query beside a duplicate one-shot child; each served command carries its own read-only option allowlist, and anything outside it falls back to a normal spawn; the child exits when its stdin closes, so it can never outlive the app. Requests whose response exceeds the 16 MiB frame limit still replace the child, but that deliberate kill no longer spends the resident's unexpected-death budget. (#972)
|
||||
|
||||
### Fixed
|
||||
- **Codex calls attributed from session metadata no longer carry a stale model.** The Buffer fast path scanned `session_meta` for the first `"model"` string anywhere in the payload, so a nested `base_instructions.provenance.model` was read as if it were `payload.model` — and since the model is last-writer-wins state, that wrong value was credited to every call before the rollout's first `turn_context` and to every call after any mid-file `session_meta` (29 of 1380 rollouts on one real corpus carry a late `session_meta`, and 57 record usage before any `turn_context`). Direct payload fields are now read depth-aware, which is what the non-fast `JSON.parse` path always did. Codex sessions re-parse once (~9s on a 4 GB rollout corpus) and the daily cache re-derives once off the warm session cache, a global re-derivation of every day and every provider since it has no per-provider invalidation; it moves per-model attribution, and clears any rollup an earlier parse change had left stale. Days whose transcripts have partly aged out are held by the never-lose guard: on a real 110-day cache no day lost value and none disappeared — 100 days came back identical and 9 grok days rose by $19.80 in total. Thanks @timdp. (#1040)
|
||||
- **A date-ranged run no longer republishes the month shards it never read.** A scoped load leaves an out-of-range month on disk, so the files it holds have no visible cache entry and the reconcile re-parses them — re-deriving the entry the shard already stores. That re-parse marked the unloaded month dirty, and the save merged and republished it under a fresh nonce name on every single run, byte-identical content and all, so a repeated `codeburn status --format json` churned old months (on a real corpus: claude/2026-03, cursor/2026-02 and warp/2026-03 renamed every run) and left the retired shards for the sweeper. A merge into an unloaded month that neither adds, changes nor removes an entry now keeps the published shard, so unchanged months keep their names and their bytes. (#1032)
|
||||
- **`models` and `audit` no longer show two identical `Grok 4.5` rows.** `grok-4.5-build` — the Grok Build harness's variant id — fell into the `grok-4.5` display entry by prefix, and since rows bucket by model id, not display name, the two came out as visually identical rows with different numbers. The variant now shows as `Grok 4.5 (build)`. Display only: no id is rewritten and no cost moves. (#1029)
|
||||
- **An upgrade no longer loses history for days whose transcripts have only PARTLY aged out.** The never-lose contract carried a cached (day, provider) slice forward only when the re-derivation found NOTHING for it, but transcripts expire per FILE rather than per day: on a day whose sources are mostly gone, a handful of turns from surviving later files still bucket onto it, so the fresh slice came back non-empty but truncated and REPLACED the full cached one. On a real cache upgrading from the last shipped daily-cache version, 2026-07-16 fell from $1,685.17 / 12,530 calls to $385.44 / 560 calls, and 13 days lost $2,765.75, 19,209 calls and 520 sessions in total. A fresh slice now replaces a settled baseline slice only when it carries at least as many CALLS - the same or more evidence; fewer calls means the source set demonstrably lost data, and the baseline is kept whole. The comparison is on calls alone: cost and tokens are re-priced accounting on the same evidence, which is exactly what a legitimate re-derivation changes (the Grok accounting fix keeps its per-day calls and is unaffected), and session counts drift down by a few on days whose sources are entirely intact. Days inside a 7-day settle window stay authoritative - their session files are still on disk, so a shrink there is a real change rather than expiry. The trade-off is deliberate and matches the direction this cache has always chosen: a future fix that legitimately REDUCES calls on a settled day keeps the older, higher value until that day is re-derived at an equal or greater call count. The timezone-change re-derive gets the exact form of the same rule - what the fresh parse can no longer explain under the old bucketing is added on top of the fresh slice instead of being dropped - and the cross-file adoption union is unchanged, where the newer schema still wins per (day, provider).
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const WORK = process.env['UPGRADE_PATH_WORK'] || join(tmpdir(), 'codeburn upgrad
|
|||
const OLD_SESSION_CACHE = 'session-cache.v7.json'
|
||||
const OLD_DAILY_CACHE = 'daily-cache.v17.json'
|
||||
const NEW_SESSION_CACHE_DIR = 'session-cache.v9'
|
||||
const NEW_DAILY_CACHE = 'daily-cache.v19.json'
|
||||
const NEW_DAILY_CACHE = 'daily-cache.v20.json'
|
||||
|
||||
const HOME = join(WORK, 'user home')
|
||||
const PAYLOADS = join(WORK, 'payloads')
|
||||
|
|
|
|||
|
|
@ -6,6 +6,24 @@ import { join } from 'path'
|
|||
import { getCodeburnCacheDir } from './cache-dir.js'
|
||||
import type { DateRange, ProjectSummary } from './types.js'
|
||||
|
||||
// Bumped to 20: the Codex fast-path read a nested
|
||||
// `base_instructions.provenance.model` out of `session_meta` as if it were
|
||||
// `payload.model` (#1040), so every call a rollout attributed from session
|
||||
// metadata - the ones before its first `turn_context`, and every one after a
|
||||
// mid-file `session_meta` - was credited to the wrong model. The codex parse
|
||||
// version and CODEX_CACHE_VERSION move with it, but the daily cache has no
|
||||
// per-provider invalidation, so days already finalized keep the wrong model
|
||||
// rows forever unless MIN_SUPPORTED_VERSION moves too. This pass re-derives
|
||||
// ALL days for EVERY provider off the warm session cache, so it costs seconds
|
||||
// rather than a full re-parse, and it is lossless: adoptOlderDailyCaches keeps
|
||||
// the superseded v19 file as the baseline, and the partial-survival guard from
|
||||
// #1033 holds any day whose sources have only partly aged out. On a real
|
||||
// 110-day cache the 19 -> 20 pass moved ZERO days down and lost none: 100 days
|
||||
// came back byte-identical and 9 grok days rose by $19.80 in total, clearing
|
||||
// rollups the grok parse change had left stale, while every codex model row
|
||||
// stayed identical (that corpus predates the `provenance` field, so the fix it
|
||||
// carries has nothing to correct there).
|
||||
//
|
||||
// Bumped to 19: Grok authoritative usage now keeps one session-level rollup
|
||||
// from top-level totals, clamps reasoning to reported output, and labels mixed
|
||||
// authoritative/heuristic coverage. Every day finalized under the previous
|
||||
|
|
@ -92,8 +110,8 @@ import type { DateRange, ProjectSummary } from './types.js'
|
|||
// that older binaries skipped. v8 added local-model savings to the daily
|
||||
// rollup; the `savingsConfigHash` field is invalidated separately when the
|
||||
// user changes their `localModelSavings` mapping.
|
||||
export const DAILY_CACHE_VERSION = 19
|
||||
const MIN_SUPPORTED_VERSION = 19
|
||||
export const DAILY_CACHE_VERSION = 20
|
||||
const MIN_SUPPORTED_VERSION = 20
|
||||
// Version-suffixed so different binaries each own a distinct file and never
|
||||
// clobber an incompatible schema. Bumping the version mints a fresh filename;
|
||||
// adoptOlderDailyCaches then unions days out of every previous file (including
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ import {
|
|||
type DailyEntry,
|
||||
} from '../src/daily-cache.js'
|
||||
|
||||
// The last SHIPPED daily-cache version before the Grok accounting change, so
|
||||
// this models the real 17 -> 19 upgrade path users hit. (v18 existed only as an
|
||||
// unreleased draft.) Anything below MIN_SUPPORTED_VERSION is untrusted, which
|
||||
// is what makes the re-derivation global rather than Grok-scoped.
|
||||
const PRE_FIX_DAILY_VERSION = 17
|
||||
// The last SHIPPED daily-cache version before the Codex session_meta model fix,
|
||||
// so this models the real 19 -> 20 upgrade path users hit. Anything below
|
||||
// MIN_SUPPORTED_VERSION is untrusted, which is what makes the re-derivation
|
||||
// global rather than scoped to the provider the bump exists for.
|
||||
const PRE_FIX_DAILY_VERSION = 19
|
||||
const cacheRoot = join(tmpdir(), `codeburn-daily-rederive-${process.pid}-${Date.now()}`)
|
||||
|
||||
function day(date: string, cost: number): DailyEntry {
|
||||
|
|
@ -67,10 +67,9 @@ afterEach(async () => {
|
|||
await rm(cacheRoot, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
// Raising MIN_SUPPORTED_VERSION re-derives EVERY day from EVERY provider, not
|
||||
// only Grok - the daily cache has no per-provider invalidation. A Grok day is
|
||||
// used here because Grok is the provider whose totals the bump exists to
|
||||
// correct; the mechanism under test is version-wide.
|
||||
// Raising MIN_SUPPORTED_VERSION re-derives EVERY day from EVERY provider - the
|
||||
// daily cache has no per-provider invalidation. Which provider the seeded day
|
||||
// belongs to is incidental; the mechanism under test is version-wide.
|
||||
describe('daily-cache re-derivation on a DAILY_CACHE_VERSION bump', () => {
|
||||
it('re-derives a day from a below-minimum v17 cache while preserving the old file', async () => {
|
||||
const date = toDateString(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue