From 087656baccf7b610d7f0add758ad964151c78ab9 Mon Sep 17 00:00:00 2001 From: iamtoruk Date: Tue, 18 Aug 2026 11:59:21 -0700 Subject: [PATCH] cache: CODEBURN_CACHE_SCOPE=all forces a full shard read The month-scoped load a ranged query takes is a behaviour change on a warm cache with no way back except deleting it. Drop the scope in loadCache, the one place every caller (including the resident serve) routes through, so a suspect scoped read can be compared against a full one in place. Read policy only: deliberately not in PROVIDER_ENV_VARS, so setting or unsetting it invalidates nothing. --- CHANGELOG.md | 2 ++ docs/architecture.md | 2 ++ src/session-cache.ts | 7 +++++++ tests/session-cache-shards.test.ts | 18 ++++++++++++++++++ 4 files changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f623e3f..7173565b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - **Applied fixes get re-measured on every `optimize` run, and told plainly whether they worked.** After `codeburn optimize --apply`, every still-applied fix comes back in an `Applied fixes` section on subsequent `codeburn optimize` runs, carrying the verdict `act report` already computes from the same reconciliation: `worked` (at least 70% of its window-scaled estimate realized), `partial` (something, but under that), `no-effect` (no measured reduction, printed with the exact `codeburn act undo ` that puts it back), or `measuring` for anything younger than the 3-day measurement window. The numbers are measured — provider-counted usage over the post-apply window — not re-estimated. `--apply` now says when the re-measure will happen, `--format json` gains `appliedFixes[]` (add-only), and the same section appears in the dashboard TUI and the desktop app. New `codeburn optimize --auto-revert` undoes the fixes that measured no reduction at all through the same code path as `codeburn act undo`; it never touches `partial` or still-measuring fixes, and never auto-reverts a `CLAUDE.md` rule (it prints the undo command instead), matching the `--yes` guardrail. - **Optimize findings say what to do with them and where their number came from.** Every finding now carries a class and a basis, and every surface groups by it: `Fix now (apply-able)` for findings `codeburn optimize --apply` can write itself, `Habits` for the behavioural ones, `FYI` for informational ones whose cost may be justified. A finding only counts as apply-able when a plan can actually be built for that instance, so an `mcp-deferral-off` caused by Vertex policy or a shell-profile override is grouped as a habit rather than promising a fix that does not exist. Alongside it, each finding is marked `measured` (summed from provider-counted usage) or `estimated` (a schema-size or recovery-fraction model), with the split reported in the header as `N measured · M estimated` in place of the blanket "Estimates only." footer. Sessions whose cost the provider never reported are kept out of the `cost-outliers` peer comparison, and a provider that only ever estimates gets the finding marked `estimated` rather than dropped. `--format json` gains `class` and `basis` per finding plus `summary.measuredSavingsUSD` (existing fields unchanged), and the new `docs/optimize.md` covers what is scanned, exactly what `--apply` may write, and how to read the health grade. +- **`CODEBURN_CACHE_SCOPE=all` forces a full session-cache read.** A ranged query reads only the month shards that can contribute a turn to it, which is a real behaviour change on a warm cache; this is the escape hatch for the case where a number looks wrong and you want to know whether the scoped read is why. Set it and every load ignores its scope and reads every shard, one-shot runs and the resident `codeburn serve` alike. It is a read policy, not an input to any cache fingerprint: setting or unsetting it re-parses nothing and invalidates nothing. + ### Added (Windows) - **`codeburn menubar` installs and launches the tray app on Windows.** The same command that installs the macOS menubar now does the Windows one, through the same pinned-release path: it resolves `windows-v`, falls back to a scan of the newest `windows-v*` release carrying both assets when that tag has none, downloads the `.msi` with the same retry and backoff, and verifies its sha256 before anything executes it — a mismatch aborts without ever handing the file to the installer. It then runs `msiexec` out of `%SystemRoot%\System32` (never a bare name, so nothing dropped next to the CLI can impersonate it) with `/i /passive /norestart`, treats exit 3010 as installed-pending-restart and 1602 as a cancelled install rather than failures, and launches the exe named by the product's Uninstall registry key. An already-installed matching version skips the download and just launches; `--force` reinstalls. - **A menubar app for Windows.** `windows/` is a Tauri 2 tray app — Rust binary, React popover — that puts today's spend in the notification area and mirrors the macOS menubar screen for screen: agent tabs, period switcher, Trend, Forecast, Pulse, Stats and Plan insights, activity and model breakdowns, optimize findings, CSV/JSON export, launch at login, currency, and theme. Windows has no menubar title, so the number lives in a second tray icon rendered from the system font at the panel's native icon size (Settings can turn it off; the tooltip always carries it). It reads everything through the CLI like the macOS and GNOME clients do, and gates on **codeburn 0.9.9 or newer** — the first release accepting `status --format menubar-json --no-optimize` — showing a setup screen with the install command until it finds one. Refresh follows popover visibility the way the macOS app does: 60 s with optimize findings while open, 2 minutes for today's total while closed, and immediately on open when what you are looking at has gone stale. The Claude quota view never spends Claude's single-use refresh token; on a 401 it re-reads Claude Code's own credential file for a token it has already rotated, matching the macOS client. Ships as an unsigned `.msi` from the `windows-v*` tag, which `codeburn menubar` now installs for you. The same crate still builds and runs a tray on Linux, but that stays experimental and unreleased — `gnome/` is the supported Linux surface. diff --git a/docs/architecture.md b/docs/architecture.md index 3b949bb4..5ec39e28 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -142,6 +142,8 @@ Three caches under `~/.cache/codeburn/` (override with `CODEBURN_CACHE_DIR`): All three use atomic write (temp file + `rename`) and write with mode `0o600`. All three carry a numeric `version` field; bumping it forces a recompute next run. +The session cache (`src/session-cache.ts`) sits beside them as a directory of per-provider-month shards. A date-ranged query reads only the shards whose months can contribute a turn to that range; `CODEBURN_CACHE_SCOPE=all` turns that off and reads every shard, whatever the range. It is a read policy only — it is not part of any provider's env fingerprint, so setting or unsetting it never invalidates the cache. + ### Optimize Detectors `src/optimize.ts` exports 20 detectors. Each returns a `WasteFinding | null`. They are composed by `runOptimize()` which collects findings, ranks them by impact, and returns them with `WasteAction` objects (paste-to-CLAUDE.md, paste-to-session-opener, prompt-now, edit shell config). diff --git a/src/session-cache.ts b/src/session-cache.ts index 3bb37344..c4d0f9e0 100644 --- a/src/session-cache.ts +++ b/src/session-cache.ts @@ -792,8 +792,15 @@ async function loadShard(path: string): Promise | nul * full: the first because its cache is the only surviving record of pruned * usage, the second because a fingerprint change discards the whole section and * must see every entry it is discarding. + * + * `CODEBURN_CACHE_SCOPE=all` is the escape hatch: it drops the scope here, at + * the one place every caller routes through, so a suspect scoped read can be + * compared against a full one without a rebuild. It is a READ policy and + * deliberately not part of any env fingerprint (PROVIDER_ENV_VARS) — setting or + * unsetting it must never invalidate a cache, only change how much of it is read. */ export async function loadCache(scope?: CacheLoadScope): Promise { + if (process.env['CODEBURN_CACHE_SCOPE'] === 'all') scope = undefined const dir = sessionCacheDir() const envelope = await readEnvelope(dir) if (!envelope) return afterMissingShardCache() diff --git a/tests/session-cache-shards.test.ts b/tests/session-cache-shards.test.ts index ff53c9b6..60ef069b 100644 --- a/tests/session-cache-shards.test.ts +++ b/tests/session-cache-shards.test.ts @@ -516,6 +516,24 @@ describe('scoped load', () => { .toEqual(['/live/apr.jsonl', '/live/jun.jsonl', '/live/mar.jsonl']) }) + it('CODEBURN_CACHE_SCOPE=all reads every month and memoizes as unscoped', async () => { + await seedThreeMonths() + clearLoadCacheMemo() + const unscoped = await loadCache() + + clearLoadCacheMemo() + process.env['CODEBURN_CACHE_SCOPE'] = 'all' + try { + const forced = await loadCache(juneScope) + expect(forced).toEqual(unscoped) + // Memoized as a full load, so a resident serve reuses it for any range. + delete process.env['CODEBURN_CACHE_SCOPE'] + expect(await loadCache(juneScope)).toBe(forced) + } finally { + delete process.env['CODEBURN_CACHE_SCOPE'] + } + }) + it('never scopes a provider whose fingerprint moved, or a durable one', async () => { const cache: SessionCache = { version: CACHE_VERSION,