diff --git a/CHANGELOG.md b/CHANGELOG.md index cafc009e..c856fb7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,7 @@ - `codeburn sync push --attribution` (opt-in): sends git attribution spans — the session→commit correlation from `codeburn yield` (`codeburn.session.attribution` and `codeburn.commit` span types with normalized repo remote, commit SHAs, merged/reverted state, and PR links). Nothing new is sent without the flag; local-only repos and Windows filesystem paths are never emitted as repo identities, and sessions whose project path no longer resolves never inherit the push-time working directory's repo. See docs/sync/README.md "Git attribution". ### Fixed (CLI) +- **Copilot claude-haiku-4.5 store rows now price correctly instead of $0.** 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 (e.g., $0.0063536 for a sampled request). Added an alias to the existing correctly-priced `claude-haiku-4-5` row (no new rate invented). The daily cache bumps from v26 to v27 to force re-derivation of already-finalized days, since this id has no self-heal via prefix fallback. (#1093) - **A pull request no longer swallows a whole repo's spend.** The working-directory correlation rule attributed every session sharing a checkout with a PR-linked session, with no time bound — a repo whose only captured PR link was pasted once attributed a month of unrelated work (129 of 131 sessions on real data) to that PR. Checkout evidence now only attributes sessions overlapping the linked sessions' own activity window. (#961) - **Shell reads finally count as reads.** `rg`, `grep`, `cat`, `git log` and friends were invisible to the read-edit-ratio detector (90%+ of real reads uncounted on bash-first workflows) while every Bash call counted as a verification step, so `edit → grep → edit` scored as rework. One shared read-shaped-command classifier fixes both detectors; unknown or mutating commands keep the old behavior. (#941, thanks @laulpogan) - **Phantom corrections from injected skill prose.** The user-correction detector matched "the wrong answer" inside a templated skill prompt, counting the same non-correction four times on real data; "answer" left the wrong- pattern list, concrete artifacts (wrong file, wrong approach) still count. (#952) diff --git a/scripts/upgrade-path/run.mjs b/scripts/upgrade-path/run.mjs index 937a0ac5..ac848043 100644 --- a/scripts/upgrade-path/run.mjs +++ b/scripts/upgrade-path/run.mjs @@ -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.v26.json' +const NEW_DAILY_CACHE = 'daily-cache.v27.json' const HOME = join(WORK, 'user home') const PAYLOADS = join(WORK, 'payloads') diff --git a/src/daily-cache.ts b/src/daily-cache.ts index 69614c66..b59561cc 100644 --- a/src/daily-cache.ts +++ b/src/daily-cache.ts @@ -6,6 +6,10 @@ import { join } from 'path' import { getCodeburnCacheDir } from './cache-dir.js' import type { DateRange, ProjectSummary } from './types.js' +// Bumped to 27: claude-haiku-4.5 copilot store rows now price correctly (alias added) — #1093. +// Previously the raw id 'claude-haiku-4.5' (tier-first, dot) from session-store.db had no +// pricing alias, so days finalized with this model were stuck at $0. No self-heal via prefix +// fallback exists for this id, so a version bump is required to force re-derivation. // Bumped to 26: copilot input/cache tokens for sessions covered by the CLI's // session-store.db move from one shutdown-rollup lump (stamped at session end) // to per-request DB rows with real timestamps, supplementary accounting calls @@ -163,8 +167,9 @@ import type { DateRange, ProjectSummary } from './types.js' // everyone, which is a lossless no-op for days already correct. // v25: #1047 activity-id pricing. v24 on main already shipped #1090. // v26: #946 copilot session-store accounting (see the top of this ladder). -export const DAILY_CACHE_VERSION = 26 -const MIN_SUPPORTED_VERSION = 26 +// v27: #1093 claude-haiku-4.5 alias (see top). +export const DAILY_CACHE_VERSION = 27 +const MIN_SUPPORTED_VERSION = 27 /// Providers whose per-day CALL COUNT means something different at /// DAILY_CACHE_VERSION 26 than it did before it. Copilot's supplementary diff --git a/src/models.ts b/src/models.ts index f080bca7..652a1924 100644 --- a/src/models.ts +++ b/src/models.ts @@ -298,6 +298,8 @@ const BUILTIN_ALIASES: Record = { 'anthropic--claude-4.5-opus': 'claude-opus-4-5', 'anthropic--claude-4.5-sonnet': 'claude-sonnet-4-5', 'anthropic--claude-4.5-haiku': 'claude-haiku-4-5', + // #1093: copilot session-store.db writes 'claude-haiku-4.5' (tier-first, dot) + 'claude-haiku-4.5': 'claude-haiku-4-5', 'claude-sonnet-4.6': 'claude-sonnet-4-6', 'claude-sonnet-4.5': 'claude-sonnet-4-5', 'claude-opus-4.7': 'claude-opus-4-7', diff --git a/tests/models.test.ts b/tests/models.test.ts index 6338dabf..5580ea0a 100644 --- a/tests/models.test.ts +++ b/tests/models.test.ts @@ -117,6 +117,19 @@ describe('getModelCosts', () => { expect(calculateCost('gpt-5.6-codex-max', 1_000_000, 1_000_000, 0, 0, 0)).toBeGreaterThan(0) }) + it('prices claude-haiku-4.5 (copilot session-store raw id), aliased to the existing claude-haiku-4-5 row (#1093)', () => { + const haiku45 = getModelCosts('claude-haiku-4.5') + const haiku45Dash = getModelCosts('claude-haiku-4-5') + expect(haiku45).not.toBeNull() + expect(haiku45).toEqual(haiku45Dash) + expect(haiku45!.inputCostPerToken).toBe(1e-6) + expect(haiku45!.outputCostPerToken).toBe(5e-6) + expect(haiku45!.cacheWriteCostPerToken).toBe(1.25e-6) + expect(haiku45!.cacheReadCostPerToken).toBe(1e-7) + expect(haiku45!.cacheWriteCostIsExplicit).toBe(true) + expect(calculateCost('claude-haiku-4.5', 1_000_000, 1_000_000, 0, 0, 0)).toBe(6) + }) + // A price override on a synthetic bare id can only be reached if the leading // segment was stripped, so these assert the namespace allowlist itself without // pinning to any real model's presence in (or absence from) the snapshot.