mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-08 08:04:32 +00:00
14 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7f2739e130
|
daily-cache: never lose history the sources can no longer re-derive (v14) (#755)
* daily-cache: never lose history the sources can no longer re-derive (v14) Session files are ephemeral (Claude Code deletes transcripts after ~30 days), so a cached day whose sources are gone exists nowhere else. Every invalidation path (schema bump, savings-config change, timezone change, incomplete-hydration retry) used to discard all cached days and re-derive from surviving sources, silently truncating history to the source retention window. Five bumps between June 22 and July 16 erased everything before April 24 on a machine with usage since March. Invalidations now re-derive what they can and carry forward every (day, provider) slice they cannot, marked 'carried'. Loading a missing or corrupt cache file adopts days from every older daily-cache file in the cache dir (legacy, versioned, .bak copies, orphaned .tmp) as a union per (day, provider), higher schema version winning per pair. Provider slices now store the full per-provider breakdown (tokens, models, categories, sessions) so carry-forwards stay exact across future rebuilds. Merge rules hardened by adversarial review: opaque pre-v5 days (totals without provider slices) merge all-or-nothing so partial parses cannot double-count into them; zero-data placeholder slices neither block nor lose carried data (session counts deduplicated by max); a partial parse never overwrites finalized baseline slices, only fills gaps; adoption purges today/future entries, applies retention, and clamps a stale lastComputedDate so a purged day cannot be skipped forever. Verified end-to-end against a copy of a real cache dir: the rebuilt v14 cache holds every (day, provider) pair present in any older cache file, including Claude days from early April that the July rebuilds had dropped. * daily-cache: per-project daily rollups in the durable record (v15) Project history previously lived only in the session layer, so it faded with the source files even though day totals now survive. Days and provider slices carry a projects breakdown (cost/calls/savings/sessions per project) filled by the day aggregator and folded through carry merges, making the By Project dimension as durable as models and categories. Days recorded before v15 keep their totals with no project split; nothing can reconstruct one once sources are gone. This is the first bump to ride the v14 carry-forward: the v14 cache is adopted losslessly and only source-backed days re-derive (verified on a real cache dir: identical totals, zero lost pairs, project splits on every derivable day). Hardening from adversarial review: placeholder-aware project session dedup so totals reconcile; migrateDays sanitizes provider slices and nested projects from foreign caches; all foreign-keyed map access in the merge path uses hasOwn reads and defineProperty writes, closing a real prototype-pollution path a regression test caught when a cache key is named __proto__. * menubar: serve headline totals and projects from the durable day set Review finding on this PR: the all-provider headline built cache-backed totals and then replaced them wholesale with a rebuild from the surviving-session parse, so current.cost/calls, the models table, and topProjects stayed truncated to the source-retention window even though history.daily carried the full record. The replacement existed only to keep the estimated-cost markers alive. The cache-backed period data is now the authority; the scan contributes exactly what day entries lack: estimated-cost markers, unpriced-model detection, per-session drill-downs, and a fresher project path. Project totals come from the same day set as the headline, with ProjectDayStats gaining a path so carried-only projects still display a friendly name. Sessions merge by max: the cache buckets a session on its start day, the scan counts it on any active day, and both undercount differently. End-to-end regression test seeds a cache whose only day is carried (no session files exist) and asserts the headline, models, and topProjects all reflect it. Verified on a real cache dir: the 6-month headline now equals the history sum to the cent. * daily-cache: close residual corrupt-input gaps in v15 ingestion sanitization v15 sanitizes provider slices and projects at ingestion and guards merge lookups with Object.hasOwn/setOwn. Three residual gaps versus the invariant (no JSON-parseable cache content may throw or produce NaN/garbage), plus one found in review: - Day-level models/categories were not sanitized: a day with models:'bad' survived load and buildPeriodDataFromDays iterated the string per-character, yielding NaN in period model totals. Day-level maps now reuse the same sanitizers as slice-level maps. - Map keys shadowing Object.prototype (constructor, toString, ...) still entered the cache from foreign files; modelTotals[name] ?? init in buildPeriodDataFromDays resolves such a key to the inherited prototype member and produces NaN. All sanitized maps now drop these keys at ingestion (deliberate tradeoff: such names are reclassified invalid). - Top-level lastComputedDate was unvalidated; a non-string later hit .slice() in the gap-start parse and threw. Kept only when it is a YYYY-MM-DD string, else null (forces a plain re-backfill). - Stale savingsConfigHash comment still described pre-v14 discard behavior; reworded to re-derive + carry forward. 3 regression tests, each verified to fail on the pre-fix code. Relevant suites 71/71 green, tsc clean. Valid-input behavior unchanged except the prototype-key reclassification above. --------- Co-authored-by: ozymandiashh <234437643+ozymandiashh@users.noreply.github.com> |
||
|
|
ae18b1fd91 |
fix: audit batch — prefetch one-shot, turn-anchored day bucketing, warm-path collapse
- prefetch fires exactly once per provider per session; memo sized to detected providers so base keys never evict (was: 12 full-history parses every 30s forever from LRU thrash re-triggering the effect) - day-aggregator buckets whole turns by user-message timestamp, matching the report/headline path: trend bars and provider breakdown now reconcile exactly (midnight-straddling turns were split); DAILY_CACHE_VERSION 13 with an IANA tz guard that rehydrates when the cache crosses timezones - overview's redundant second all-provider scan collapsed (~11% faster warm); numbers parity-guarded - lock takeover cleans stale hydrating.lock; SIGINT/SIGTERM release it - scanner skips EPERM/EACCES provider dirs (progress shows skipped) instead of aborting; onboarding hints Full Disk Access - rateLimited quota copy render tests; export -f json emits valid empty JSON when no data Root 2023 (+4), app 327 (+3), typechecks clean. |
||
|
|
e9655fa03c |
fix: hydration completeness markers, cold-only splash, honest switching, prefetch
P0: an interrupted cold hydration left partial caches that the emptiness check read as warm — the daily backfill froze (missing older days) while session parsing healed gradually (drifting totals), poisoning every surface sharing the cache. Both caches now carry an explicit complete marker: partial resumes cold under the hydration lock; unmarked caches self-heal with one re-hydration. Regression test seeds the exact frozen artifact and asserts bit-for-bit convergence with a never-interrupted run. - splash indexing reveals only on genuinely cold scans (explicit cold flag in the progress protocol), never on warm incremental re-parses - uncached filter switches clear to skeleton instead of showing the previous filter's numbers; cached switches paint same-commit; background prefetch warms every detected provider after idle - build stamp (git sha + date) in splash and About ends build ambiguity - payload parity test: identical payloads on identical cache, and payload totals equal the CLI report path - Sessions empty state keeps the provider filter row; zero savings line hidden; TCC usage-description strings + Full Disk Access docs - warm profile documented: optimize scan ~1.47s dominant (safe refactor deferred), parse ~1s, aggregation ~30ms Root 1848, app 320, typechecks clean. |
||
|
|
300e949951 |
feat(app): refresh cadence, instant switching, cache coherence, telemetry v1
Performance and coherence: - Settings > General 'Refresh every' (Manual/30s/1m/3m/5m/10m), live via RefreshCadenceContext; Manual polls only on demand - usePolled memoKey LRU: provider/period switches paint the last-good result instantly with a switching hairline while refreshing quietly - quota TTL 5min + honest rate-limited copy on 429 backoff - version-suffixed cache files (session-cache.v5.json, daily-cache.v12, auto-minted on future bumps); legacy files never written or deleted, adopted once when versions match: old and new binaries coexist without clobbering (field-observed menubar-vs-desktop ping-pong) - advisory hydration lock: concurrent cold starts share one scan (wait-then-read-warm), stale/dead locks self-heal, never a correctness gate Telemetry v1 + onboarding (desktop only, per owner decisions): - first-launch onboarding (3 feature screens + consent); region-split default (EU/EEA/UK/CH off, elsewhere on, unknown off); nothing sends before consent completion or while off; dev builds never send - anonymous install UUID, rotated on opt-out; day-granularity events, cost buckets only; whitelisted names; 200-event queue, 5min flush - Settings > Privacy live toggle replaces the static claim Zero computed-number changes. App 316/316, root 1805. Wire contract targets api.codeburn.app/v1/telemetry (Worker follows). |
||
|
|
75c32e6d65
|
fix: fix and improve test isolation and collision with environment (#530)
* fix: fix and improve test isolation and collision with environment * docs: remove unnecessary comment * test(env-isolation): clear CODEBURN_FORCE_MACOS_MAJOR and pin TZ Two env vars read in src/ were not isolated: CODEBURN_FORCE_MACOS_MAJOR (now cleared so it cannot leak between tests) and TZ (now pinned to UTC, since clearing it falls back to the OS zone and would shift date buckets versus a clean CI runner). --------- Co-authored-by: AgentSeal <hello@agentseal.org> |
||
|
|
7f44eeb1ba |
fix(daily-cache): purge cached today/future entries on hydration
Keeps the perf win from PR #486 (yesterday stays cached, no re-parse every run) but restores a narrow safety guard: drop any cached entry dated today or later before computing the gap range. The cache only ever stores complete past days, so a >= today entry can only come from the clock moving backward or a stale older cache; without this it would be served frozen instead of being recomputed live. Yesterday and earlier are untouched, so this does not re-parse already-cached days. Adds a regression test. |
||
|
|
0ac09d8909 |
fix(menubar): reduce repeated status parsing
Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
8ded6ad6fd |
feat(cli): track local-model cost savings against a paid baseline (#421)
Add a new `localModelSavings` config and `codeburn model-savings` CLI that maps a local-model name (e.g. llama3.1:8b) to a paid baseline (e.g. gpt-4o). The local call still costs $0; the new `savingsUSD` field tracks the counterfactual spend avoided by running locally and is reported separately from `costUSD` everywhere a number is shown. * Parser normalization (`applyLocalModelSavings`) runs on Claude parse, direct provider calls, and the cached-call path. It forces `costUSD` to 0 and attaches `savingsUSD` + `savingsBaselineModel` + `isLocalSavings` on the `ParsedApiCall`. Local-savings wins for actual cost even when the same model is also in `modelAliases`. * Session, project, day, model, category, activity, skill, and subagent rollups all carry `savingsUSD` alongside `costUSD`. * `status --format json` adds `today.savings` and `month.savings`. * `status --format menubar-json` adds a `current.localModelSavings` block (totalUSD, calls, byModel, byProvider) plus savings on topModels, topProjects, topSessions, topActivities, and history daily entries. Schema fields default-decode for backward compat. * `report --format json` adds savings across overview/daily/ projects/models/activities/skills/subagents/topSessions, with the active paid baseline name on each model row. * `models` command gains a `Saved` column on table/markdown/CSV and a `savingsUSD`/`savingsBaselineModel` pair in JSON. Default `--min-cost 0.01` filter now ORs in `savingsUSD >= minCost` so local models with $0 actual cost but >0 savings still surface. * CSV/JSON exports add a `Saved (CODE)` column on summary/daily/ models/projects/sessions. * Dashboard TUI shows a green 'saved $X by local models' footer line in the overview when any savings are present. * macOS Swift payload gains a `LocalModelSavings` Codable block and savings fields on every model/activity/session/daily struct. Hero shows a green leaf 'Saved $X' caption, models section gets a green `Saved` column. `swift build` clean. * GNOME indicator adds 'saved $X' to the hero meta line and a `codeburn-model-saved` column to the model row. * Daily cache schema bumped to v8 (`savingsUSD` on day/model/ category/provider). `savingsConfigHash` invalidates the cache when the user changes their baseline mapping so historical saved-spend numbers never lie about a stale baseline. * Defensive `Object.hasOwn` lookup in `getLocalSavingsBaseline` blocks the prototype-pollution test that previously surfaced via the savings path with a hostile `__proto__` model name. * New tests (5 files, 25 tests, 549 lines) cover pricing helpers, end-to-end parser normalization, day aggregator savings, menubar payload savings, CLI set/list/remove, and daily-cache hash invalidation. Existing tests for daily-cache / day-aggregator / models-report updated for the new fields. Full vitest suite: 1028/1028 passing across 73 test files. `tsc --noEmit` clean. `npm run build` clean. (Note: `mac/Tests` has a pre-existing `no such module 'Testing'` environment error on the installed Swift toolchain, confirmed on `main` before this PR; not caused by these changes.) |
||
|
|
c85beeaeae
|
Fix Claude 1-hour cache write pricing (#317)
Co-authored-by: ozymandiashh <234437643+ozymandiashh@users.noreply.github.com> Co-authored-by: iamtoruk <hello@agentseal.org> |
||
|
|
d142bd97ef
|
daily-cache: discard pre-v5 caches (fixes menubar providers regression) (#297)
PR #296 (Cursor per-project breakdown) bumped DAILY_CACHE_VERSION from 4 to 5 but left MIN_SUPPORTED_VERSION at 2. The migration path (isMigratableCache + migrateDays) only fills in missing default fields; it does NOT recompute the providers / categories / models rollups from session data, because raw sessions are not retained in the cache. So a v4 cache migrated to v5 carried forward its old per-day provider totals (single 'cursor' bucket) for the full retention window. Effect on users post-#296: the macOS menubar's `current.providers.cursor` would show the orphan-bucket subtotal instead of the full Cursor cost for any historical day whose daily entry was computed before #296 landed. Live-test on my machine showed cursor=$3.78 against a migrated v4 cache vs cursor=$4.08 (correct) after the daily cache was discarded — the $0.30 gap was the workspace projects whose costs were no longer aggregated under the 'cursor' label by the new code. Fix: raise MIN_SUPPORTED_VERSION to 5 so any cache with version < DAILY_CACHE_VERSION is renamed to `.bak` and the cache is recomputed from scratch on next run. The recompute is the same operation that backfills the cache for a new user, so the cost is a one-time cold-path hit (~3s on the test machine). Test for the migration case updated to assert the new discard-and-bak behavior. Full suite: 46 files / 654 tests pass. |
||
|
|
afd0ee7011
|
Validator hardenings on the bug-hunt batch (#254)
* Five correctness fixes from multi-agent bug hunt
A multi-agent audit of the codeburn correctness surface found five
real bugs each producing visibly wrong numbers or risking data loss.
All five fixes were validated by parallel review agents and exercised
end-to-end against real session data on this machine.
- src/cli.ts: --refresh <seconds> was using bare parseInt as the
commander callback. Commander invokes the callback as
parseInt(value, previous), so previous becomes the radix:
--refresh 30 was being parsed as parseInt('30', 30) = 90, and
--refresh 60 became NaN. Replaced with parseInteger (already
defined at line 48 with radix locked to 10) at all three sites.
- src/providers/cursor.ts: parseAgentKv was timestamping every
agentKv call as new Date().toISOString() because the Cursor
SQLite schema has no per-message timestamp. Result: every
Cursor agent call regardless of when it happened landed in
today's date bucket. Now uses statSync(dbPath).mtimeMs as a
bounded ceiling so calls land at the actual last-write time of
the Cursor database, not today. Verified locally: a 1904-call
Cursor history with March 22 mtime now correctly bucket into
all-time only and shows 0 calls for today/week/30days.
- src/providers/codex.ts: prev token counters were only updated
inside the cumulative-fallback branch, so a session emitting N
events with last_token_usage followed by one cumulative-only
event computed the next delta against prev=0 and double-counted
the entire cumulative window. Cost could be inflated 10-100x
for any mixed-format Codex session. Now prev advances to the
current cumulative state regardless of which branch ran.
- src/providers/gemini.ts: totalOutput accumulated output+thoughts
while totalThoughts was tracked separately. The result was
outputTokens = output+thoughts AND reasoningTokens = thoughts;
any consumer summing the two double-counted thoughts. Now
totalOutput holds just output, reasoningTokens holds thoughts,
and the cost calc folds thoughts into the output count to keep
pricing correct (Google bills thoughts at the output rate;
calculateCost has no reasoning parameter).
- src/export.ts: exportJson had no safety check before writeFile,
so codeburn export -f json -o ~/important.json would silently
clobber the user's file. CSV path had a marker-file guard; JSON
did not. Now refuses to overwrite a file unless its first 4KB
contain the codeburn schema marker. Uses a streaming partial
read so a large existing file does not OOM Node's ~512MB
string limit. Refuses directories outright.
Skipped intentionally: cursor-auto/copilot-auto/cline-auto/
qwen-auto are aliased to claude-sonnet-4-5. The audit flagged
this as wrong pricing for non-Anthropic auto-routed turns, but
Cursor's "auto" mode does not expose the actual model and any
alternative estimate is equally arbitrary. README already
documents this as a Sonnet-based estimate.
vitest run: 38 files, 529 tests pass.
* Five more correctness fixes from the bug-hunt round
This commit closes out the remaining critical-tier findings from the
multi-agent audit, with one item documented as a known limitation.
- src/providers/cursor.ts: bubble dedup key included mutable
inputTokens/outputTokens. Cursor mutates token counts on the row in
place when streaming completes, so re-parsing the same DB produced
a fresh dedup key per bubble and silently double-counted. Switched
to the SQLite row key (`bubbleId:<unique>`) which is stable per
bubble. Adjusted BubbleRow type and BUBBLE_QUERY_BASE to expose
`key as bubble_key`.
- src/providers/pi.ts: usage fields were destructured non-optionally,
but real Pi/OMP session files sometimes omit individual fields.
`calculateCost(model, undefined, ...)` returned NaN, and that NaN
propagated into every aggregate cost total. Coerce each field to
0 with `?? 0`.
- src/models.ts: getShortModelName and the getModelCosts startsWith
fallback both walked the dictionary in insertion order. A model id
like `gpt-5-mini` could resolve to the entry for `gpt-5` (matched
by startsWith first) and silently get GPT-5's display name and
pricing tier. Iterate longest keys first so more-specific prefixes
win. Tightened the cost fallback's match condition from
`startsWith(key) || startsWith(key + '-')` to require either an
exact match or a `key + '-'` continuation, removing accidental
matches like `gpt-50` against `gpt-5`.
- src/models.ts: calculateCost returned 0 silently for any model
missing from the pricing snapshot. New Anthropic / OpenAI models
shipped between snapshot refreshes look free until the user
notices. Now warns once per unknown model name per process to
stderr. Skips the warning for the `<synthetic>` placeholder so
the noise floor stays low.
- src/yield.ts: revert detection was broken on the canonical case.
Two problems: (1) `subject.toLowerCase().includes('revert')`
matched any commit whose subject mentioned the word ("Add revert
button" was misclassified). (2) The window logic only counted
reverts within the original session's 1-hour boundary, but real
`git revert` commits land in later sessions, so original sessions
always looked productive. Now: getRevertedShas runs once with
`--grep=^This reverts commit` and parses bodies to build a Set of
SHAs that were the target of a revert anywhere in history.
CommitInfo.wasReverted is set when this commit's SHA appears in
that set. categorizeSession then flags a session as reverted when
its in-main commits were later reverted, regardless of when the
revert itself happened.
- src/providers/droid.ts: SKIPPED with comment. Droid records token
usage only at session level. The current behavior splits evenly
across emitted assistant calls and prices all of them at
settings.model (the latest model). For sessions where the user
switched models mid-stream, costs are approximate. Added an
inline comment documenting this; a real fix requires per-message
model data that isn't in the Droid JSONL schema.
Verified end-to-end on this machine:
- vitest run: 38 files, 529 tests pass
- `codeburn report --format json` produces valid JSON
- `codeburn yield -p week` runs without crashing, finds 0 reverts
in the user's recent git history (plausible — fix changed the
detection from "subject contains revert" to "this commit's SHA
appears in a later 'This reverts commit ...' body")
- Stderr now warns for unknown model ids: `openai/gpt-5.3`,
`qwen3.6:35b-a3b-bf16`, `big-pickle`. These previously priced
silently at $0.
* Four high-severity fixes from the bug-hunt round
- src/currency.ts: getExchangeRate wrapped fetchRate and cacheRate in
one try/catch. If fetchRate succeeded but cacheRate threw (disk
full, ENOSPC, no permissions on the cache dir), the catch block
swallowed the error and returned 1. Every cost rendered after that
point became USD-equivalent silently. Now the fetch and the cache
write live in separate paths: a successful fetch returns the rate
even if the persist fails, and the cache-write error is dropped to
a fire-and-forget so transient disk problems do not corrupt the
user's currency display.
- src/cursor-cache.ts: writeFile was non-atomic. Two concurrent
codeburn invocations writing to cursor-results.json could
interleave bytes mid-write, leaving a truncated file that
parsed-error on next read and forced a full SQLite re-scan every
run. Switched to the temp-file + rename pattern with a randomized
temp name so each writer gets its own staging file and the rename
is atomic on POSIX. Crash mid-write also leaves only a leftover
temp file, which gets unlinked in the catch path; the destination
is never half-written.
- mac/.../CodeBurnApp.swift refresh loop on sleep: the loop's
Task.sleep keeps a wakeup pending across system sleep, so on wake
the natural tick fires the same instant the wake observers do.
Combined with didWakeNotification, screensDidWakeNotification, and
the launchd com.codeburn.refresh distributed notification, that
produced 2-3 concurrent CLI spawns within ms of every wake. Now:
willSleepNotification cancels the loop task; didWakeNotification
restarts it. The loop also reads lastRefreshTime and skips its
natural tick if a wake/manual/distributed-notification refresh ran
within the last 5 seconds, coalescing the two sources of refresh
into one CLI spawn per wake event.
- mac/.../CodeBurnApp.swift observeStore: the read closure had an
implicit strong self capture (it accessed store.* without a
capture annotation), pinning self for the lifetime of any
unfired observation. Added [weak self] and a guard to make the
capture explicit. withObservationTracking is one-shot per call,
so there is at most one active subscription at a time; the
earlier audit's claim of an unbounded leak overstated the issue,
but tightening the capture pattern is still cleaner.
Verified:
- vitest run: 38 files, 529 tests pass
- swift build -c release --arch arm64 --arch x86_64: clean, no
diagnostics, no MainActor warnings
- mac/Scripts/package-app.sh dev produces a valid universal bundle
- Menubar launches and runs without crash
* Eleven medium-severity fixes from the bug-hunt round
- src/format.ts formatTokens: guard against Infinity, NaN, and
negative input. Previously a corrupt aggregate could leak into
the UI as the literal strings "NaN" or "Infinity". Negatives now
render as "0" rather than "-500" with no scaling.
- src/cli-date.ts parseDateRangeFlags: the missing-from default
was new Date(0), which opened a 55-year scan from 1970 epoch
whenever the user passed only --to. Default now anchors at 6
months back from now, matching the dashboard's all-time period.
Test updated to assert the new bounded window.
- src/cli-date.ts toPeriod: previously fell back silently to "week"
for any unknown input, so a typo like `-p mounth` produced a
quiet 7-day report while the user thought they were viewing the
month. Now exits with a clear stderr error and exit code 1.
Test updated to assert the loud-failure behavior.
- src/optimize.ts urgencyScore: rebalanced weights so a high-impact
finding with zero observed tokens cannot outrank a medium-impact
finding with millions of tokens. Old 0.7/0.3 split made high+0
(0.70) beat medium+1B (0.65). New 0.5/0.5 split makes medium+1B
(0.75) beat high+0 (0.50). Token normalization lifted to 5M so
the ramp covers a realistic spend range.
- src/models.ts calculateCost: clamp negative or non-finite token
inputs to 0 before pricing. A corrupt JSONL emitting a negative
count would otherwise produce a negative cost that silently
subtracted from real spend in aggregates.
- src/currency.ts convertCost: stop rounding during aggregation.
For zero-fraction currencies (JPY, KRW, CLP) this clamped every
per-session cost to a whole unit before sum, so a project of
1000 sessions averaging ¥0.4 each aggregated to ¥0 instead of
¥400. formatCost still rounds at the display boundary.
- src/config.ts saveConfig: the temp file path was a fixed
`${configPath}.tmp` suffix. Two simultaneous saveConfig calls
(overlapping menubar and CLI runs) raced on the same staging
file and could leave one writer reading partial bytes from the
other. Randomized the temp suffix per call.
- src/providers/antigravity.ts flushCache: the early return on
`!cacheDirty` short-circuited eviction when liveCascadeIds was
supplied but no cascade had been added or updated this run. As
a result, deleted .pb files persisted in the cache forever once
the user stopped writing to it. Eviction now runs whenever
liveCascadeIds is provided, marks the cache dirty if anything
was removed, and only then short-circuits if there is nothing
to write.
- src/daily-cache.ts addNewDays: cap retention at 2 years. The
days array previously merged forever, growing the cache file by
hundreds of bytes per day until JSON parse on every CLI
invocation became measurable. The 6-month UI period plus the
365-day BACKFILL_DAYS bootstrap both fit comfortably inside the
cap, with headroom for a future longer window.
- src/dashboard.tsx useInput: period number keys (1-5) and arrow
keys triggered a reload while the compare view was mounted. The
parent's data state changed underneath the user with no visual
affordance back to the dashboard. Now those keys are gated on
view !== 'compare', and `b` / Esc inside compare returns to the
dashboard.
- mac/.../HeatmapSection.swift formatters: prettyDate, buildTrend
Bars, computeTrendStats, computeForecast, and computeAllStats
each allocated a fresh DateFormatter (and Calendar) on every
call. SwiftUI re-evaluates these views many times per second
during hover scrubbing on the trend chart, so the allocations
were a measurable hot spot. Lifted the yyyy-MM-dd / "EEE MMM d"
/ "MMM d" formatters and the gregorian Calendar to fileprivate
cached singletons.
Two findings from the same bucket were not addressed here:
- UpdateChecker SHA-256 / codesign verification is already
performed by src/menubar-installer.ts (verifyChecksum at line
85). The Swift side just kicks off `codeburn menubar --force`
which runs that path. The audit's claim of missing verification
was a misread.
- NSDistributedNotificationCenter sender validation: the
`com.codeburn.refresh` listener accepts from any sender, but
forceRefresh has a 5-second rate-limit gate so the abuse
ceiling is one CLI spawn per 5 seconds. Mitigations (Mach IPC,
per-launch shared secret) are disproportionate to the impact.
vitest run: 38 files, 529 tests pass.
swift build -c release: clean, no warnings.
* Validator hardenings on the bug-hunt batch
Hoist the per-call sort in getModelCosts and getShortModelName to module
scope so model lookups on the hot path stop reallocating sorted key arrays.
Sanitize the unknown-model stderr warning by stripping C0/C1 controls
and capping length, so a hostile or corrupt JSONL cannot inject terminal
escape sequences via the model field.
Skip the daily-cache prune when newestDate fails to parse. The previous
code produced a NaN cutoff and silently dropped every cached day on the
next merge.
Adds tests locking down the stable resolution of common model names
(gpt-5-mini vs gpt-5, claude-haiku-4-5 vs claude-3-5-haiku, etc.) and
the prune NaN guard.
|
||
|
|
888f592bd2 |
Make daily cache durable: hydrate from all commands, migrate instead of nuke
- Extract ensureCacheHydrated() from menubar-json path into daily-cache.ts - Call it from every command that parses sessions (report, status, today, month, export, optimize, compare, yield) so CLI-only users also persist historical data that survives source file deletion - Replace strict version equality check with fill-defaults migration for cache versions 2-4, preserving history across schema changes - Back up old cache to .bak before discarding on unmigrateable versions - Fix Copilot auto bucket display names in menubar (Copilot (Anthropic), Copilot (OpenAI)) - Fix Roo Code / KiloCode provider key matching in menubar tab strip |
||
|
|
888030fce3 |
fix: recompute yesterday in daily cache to prevent stale menubar data
The daily cache never re-processed yesterday once cached, so a mid-day run would freeze partial cost/call data permanently. The "All" provider path in menubar-json relied on this cache, causing the menubar to show wildly incorrect numbers while per-provider views (which parse fresh) were correct. Now yesterday is evicted and recomputed on every run, and addNewDays upserts instead of skipping duplicates as defense-in-depth. |
||
|
|
495a254338 |
feat(mac): native Swift menubar app + one-command install
Introduces mac/ with a native SwiftUI menubar app that replaces the previous SwiftBar plugin entirely. Install via `npx codeburn menubar`, which downloads the .app from GitHub Releases, strips Gatekeeper quarantine, and drops it into ~/Applications. Highlights - mac/ SwiftUI app: agent tabs, Today/7/30/Month/All period switcher, Trend/Forecast/Pulse/Stats/Plan insights, activity + model breakdowns, optimize findings, CSV/JSON export, Star-on-GitHub banner, live 60s refresh, instant currency switching with offline FX cache. - Security: CodeburnCLI argv-based spawn (no shell interpretation), SafeFile symlink guards + O_NOFOLLOW writes, FX rate clamping to [0.0001, 1_000_000], keychain filtered to account == "default", removed byte-window credential log, in-flight refresh guard, POSIX flock on config.json writes, TerminalLauncher validates argv before AppleScript interpolation. - Performance: shared static NumberFormatter (thousands of allocations per popover redraw eliminated), concurrent pipe drain with 20 MB cap + 60s timeout in DataClient, Observation-tracked reactive UI, 5-min payload cache keyed on (period, provider). - CLI: new `codeburn menubar` subcommand that downloads + installs + launches the .app (no clone, no build). New `status --format menubar-json` payload builder. `export` rewritten to produce a folder of one-table-per-file CSVs with a `.codeburn-export` marker so arbitrary -o paths cannot be silently deleted. - Removed: src/menubar.ts (SwiftBar plugin generator), install-menubar / uninstall-menubar subcommands, `status --format menubar` directive output, tests/menubar.test.ts, tests/security/menubar-injection.test.ts. - Release: .github/workflows/release-menubar.yml builds universal binary, assembles .app, ad-hoc signs, zips, uploads on mac-v* tag push. Runs on the free macos-latest runner. Tests - 230 TypeScript tests pass - 10 Swift CapacityEstimator tests pass - TypeScript typecheck clean - Swift release build clean |