path.startsWith('/') POSIX guard rejected the Windows bundled-CLI path
(C:\...\launch.js), so 100% of Windows installs got not-found
(confirmed in telemetry: 84 events, all win32/x64). Use path.isAbsolute
everywhere. cli_error now carries {cmd, kind, detail?} for
self-diagnosing errors, never paths/args/stderr.
Retires the binary-digit flame (which muddied below ~256px) in favor
of the original solid flame everywhere: desktop app icon + FlameMark
(splash, sidebar, About, dock), menubar dock icon, brand logo. One
consistent mark that stays crisp at every size, matching the website
favicon and local dashboard.
The dashboard header logo and favicon render at 24px where the binary
flame muddies; use the solid flame like the site favicon. Menubar dock
icon keeps the binary squircle (large-icon context).
Local web dashboard favicon + header (dash/public/codeburn-logo.png)
and the menubar dock/app-icon source (assets/menubar-logo.png,
assets/logo.png) now use the binary-flame mark, matching the desktop
app and website. The menu-bar status item stays monochrome-template by
Apple convention; the new dock icon lands on the next menubar release.
The AppImage-only Linux build hit users with a no-application-installed
error on double-click, because AppImages do not self-register and need
chmod plus libfuse2 on newer distros. Add deb and rpm electron-builder
targets so Linux users get a native install that lands in the app menu
with no manual steps; the deb ships a .desktop entry for exactly that.
README hero grid now lists Debian/.deb, Fedora/.rpm, and AppImage
alongside the macOS and Windows buttons, and the desktop release notes
explain the deb/rpm path plus the AppImage FUSE requirement.
Checks the GitHub releases for newer desktop-v tags once per launch
and every 24h (15s timeout, no identifying headers, offline = silent
retry). A dismissible banner and a Settings About row surface the
newer version and link the release page; dismissal persists per
version so each release nags at most once. No auto-install: unsigned
builds cannot, by platform constraint; the checker only notifies.
App 364/364 (+24), typechecks + build green.
Bump the version and refresh the bundled LiteLLM pricing snapshots.
Since 0.9.15, roughly sixty merged changes: the /advisor escalation
billing and unified large-line scanners for Claude, Copilot CLI
session.shutdown billing and skill extraction, the CodeWhale provider,
kiro v2 credit pricing, OpenCode custom dirs and legacy-store merging,
archived Codex sessions, antigravity timestamp stability, estimated-cost
markers across every surface, the doctor command, spend budgets, act
apply-model with a per-project quality tripwire, honest yield attribution
with an ambiguous bucket, MCP tool-deferral detection and fixes, agent by
model cross-breakdowns and per-record exports, granular dashboard
timelines, a scrollable daily history, the Electron desktop app with mac,
Windows and Linux packaging, menubar rate-limit handling and idle-cadence
energy fixes, team sync in preview, and the four-surface README.
Every merged PR was re-verified against this exact tree before the bump:
full JS suite (3527 tests), Swift suite (97), release build smoked, and
eight area audits with live functional probes.
* feat(optimize): mcp-deferral-gaps finding family
Add three read-only detectors for MCP tool-def deferral coverage gaps:
- mcp-deferral-off: sessions carrying MCP tool-def overhead with zero
ToolSearch invocations and no deferred_tools_delta inventory across
the window. Cause attribution, each with its own message and fix:
stale ENABLE_TOOL_SEARCH=false (settings env, any scope, or shell
profile), non-first-party ANTHROPIC_BASE_URL (reported as unknown
proxy — never assumes capability), Vertex AI config, all observed
Claude Code versions predating default-on tool search (v2.1.7), or
a generic deferral-appears-inactive fallback.
- mcp-alwaysload-hygiene: alwaysLoad-pinned servers whose observed
call rate is below 1 call per 5 sessions (named constant). Notes the
5s startup-blocking cost of alwaysLoad in the explanation.
- mcp-defer-threshold: ENABLE_TOOL_SEARCH=auto/auto:N overrides whose
threshold the estimated def volume never reaches, so tools load
upfront; recommends the tightest auto:N that would defer.
Detection only: no FindingApply payloads, no act-layer changes, no
file mutation. Users without deferral gaps see zero new output —
deferral-active evidence (ToolSearch calls or inventory) suppresses
the findings, and each detector gates on conservative named
thresholds. Config readers take an injectable homeDir (PlanContext
style) for hermetic tests.
Discrepancies vs the design notes, codebase/live-docs win:
- The Claude Code changelog records default-on (2.1.7), not first
ship, so the version cause uses the default-on boundary.
- Per-tool "anthropic/alwaysLoad" lives in server-served tool _meta,
not static config; server-level only, limitation documented.
- Live docs confirm auto default threshold 10% of context window and
the alwaysLoad v2.1.121+ requirement.
Refs getagentseal/codeburn#614
* feat(act): defer-* plan kinds for native deferral config
Wire the mcp-deferral-gaps findings to the act machinery with three
new plan kinds, all through the existing ConfigDocs/runAction path
(journaled, backed up, stale-guarded, dry-run previewable, undo
restores byte-identical files):
- defer-enable: removes a stale ENABLE_TOOL_SEARCH=false from the
settings env of the scope where the finding recorded it. Refusal
paths render as manual notes instead of plans: shell-profile lines
(codeburn only appends marker blocks to shell files, never edits
user lines), unknown proxies (setting the override blind makes
requests fail outright on proxies that don't forward tool_reference
blocks — the note says to verify first), Vertex, and old versions.
A proxy-verified cause (set by the part-3 verifier) produces a real
ENABLE_TOOL_SEARCH=true plan in user settings.
- defer-alwaysload: strips alwaysLoad: true from the named servers in
the exact config files the finding recorded. Gated on an injectable
installed-version probe (default: claude --version); below v2.1.121,
unparseable, or probe failure all refuse with a note naming the
required version. Preview notes the removed up-to-5s startup block.
- defer-threshold: rewrites the auto override to the recommended
auto:N, or deletes it when the finding says the default already
defers (removeOverride).
Findings now carry FindingApply payloads (path, scope, cause, servers,
recommended N); detector text is unchanged, so plain optimize renders
byte-identically to before. Every plan links its findingId into the
ActionRecord and states that changes take effect on the next session
(the config is read at Claude Code start).
Discrepancy vs the design notes: no existing version-check helper was
found in guard/ or act/ to reuse, so the detector's parseVersion/
versionPredates are exported and shared instead of adding a parallel
comparator.
Refs getagentseal/codeburn#614
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
Sessions launched from monorepo subdirectories or separate worktrees
of one repository formed independent attribution groups, each awarding
the same commit (double-counted productive sessions; the duplicate
owner was not even flagged ambiguous). Groups now key on the realpath
of git-common-dir, so all subdirs and worktrees of a repo collapse
into one group while distinct repos stay separate; ambiguity semantics
unchanged, now correctly spanning the whole repo. rev-parse results
cached per directory.
Fixes#713. Regression tests fail on the unfixed code.
Energy (field report: ~3x Chrome drain):
- polls skip entirely while the window is hidden/minimized, with one
catch-up refresh on return when data is stale; visible-but-unfocused
keeps polling (second-monitor case)
- all looping animations pause under html.page-hidden (the sidebar
flame flicker was a perpetual compositor drain)
- default cadence 60s; an explicit stored choice is always honored
Currency (field report: set USD, still saw EUR):
- memo-served payloads re-applied their embedded stale currency; config
mutations now purge the renderer memo and force-refresh, and a
switching payload can never overwrite the applied currency
- USD default verified end to end
Measured: hidden window = 0 new CLI spawns over 5 cadences (was: full
polling forever). App 340/340, build + package green.
* docs(readme): four-surface hero grid with per-surface install shortcuts
Replace the single TUI screenshot and the scattered optimize, compare and
old menubar shots with one 2x2 grid under the badges: Desktop, Web,
Terminal, and macOS Menubar, each with its one-line way in (direct
download badges for the desktop builds, npx codeburn web, npx codeburn,
codeburn menubar). New screenshots downscaled to 1600px so the README
stays light.
* docs(readme): star and sponsor ask up top, consistency pass
Adds a one-line ask under the badges: star the repo if CodeBurn showed
you something the bill never did, sponsor to keep 36 integrations
honest. Caption line under the hero grid.
Consistency fixes from a full read: the sponsor section said 30
integrations while everything else says 36; dx is the Dioxus CLI, not a
package runner, so the install line now says pnpm dlx; the by-agent
command row was overstuffed for a table cell; Opus 4.6 in the signals
table aged into Opus 4.8; the License section ran MIT and the affiliation
note into one paragraph; two stray em dashes and a double blank line.
Follow-ups to #710, completing issue #708's second ask plus the two review
notes:
- export: new records.csv with one row per call incl. subagentType and
model; sessions.csv gains both fields APPENDED after all legacy columns
so by-index consumers are unaffected; JSON fields additive; formula-
injection escaping preserved
- models --by-agent: the main-session bucket sentinel becomes '(main)' -
agentType is free-form text, so a real subagent literally named 'main'
no longer merges into the main bucket (test-pinned across all formats)
- README: note that the --min-cost 0.01 default can hide sub-cent agent
buckets; --min-cost 0 shows the complete agent inventory
Orchestrator setups pin different agents to different models (planner on
Opus, implementers on Sonnet, scouts on Haiku). The dashboard shows
"Skills & Agents" and "By Model" as separate panels, so there was no way
to see which agent drove which model's spend, or catch an agent that
silently switched models. `codeburn models --by-agent` closes that gap:
one row per (provider, model, agent), mirroring the existing --by-task
pattern across the table, markdown, CSV, and JSON renderers.
The agent label comes from each Claude subagent transcript session's own
agentType crossed with that session's model calls, aggregated through the
same assistantCalls path --by-task uses (not modelBreakdown), so cache-tier
dedup, savings, and Codex-credit logic all carry over unchanged. This is
the authoritative pairing; main-session subagentTypes spawn markers are
deliberately not joined to models, since a spawn marker names an agent but
not the model that agent actually ran on.
Ordinary main sessions (no agentType) and every non-Claude provider have
no agent, so their spend buckets under the label "main" rather than being
dropped or fabricated into a false agent. --by-agent is mutually exclusive
with --by-task; passing both errors politely.
Exposing subagentType + per-record model on `codeburn export` (so users
can build custom crosses over the export) is a separate follow-up and is
intentionally out of scope here.
- 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.
Coalesced overview polls joined to one stuck cold child each emitted
their own cold_start on the shared settle, measured from their own
start times — producing laddered pseudo-durations in telemetry.
Launch-scoped latch emits at most once, on the first cold attempt's
settle, with that attempt's duration; timedOut wiring and local-day
stamps verified already correct and locked with regression tests.
App 324/324.
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.
A 4xx response means the server will never accept that payload;
retrying it wedges the queue at its cap and blocks all newer events.
Drop on 4xx, keep retrying on 5xx and network failures. Matches the
server's validation contract (batch cap 200).
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).
Ported onto current main after the locale-grouping change in overview.ts;
review follow-ups included:
- explicit note: budgets count the full would-be API cost including
subscription-proxied spend, consistent with the overview Cost total
- percent display floors the raw value on both surfaces, so 99.6 percent
renders as 99 percent in warn state and 100 percent appears only when the
state is over; regression tests cover the boundary on overview and --check
- overview keeps main's en-US locale-grouped formatting and the
unpriced-model warning block; the budget line renders after it
* feat: implement act apply-model and report baseline tripwire (#607)
* feat: implement act apply-model and report baseline tripwire (#607)
* fix(act): scope model-default tripwire to the applied project
Review fixes for #616:
- modelDefaultRow and the under-20-edit-turns gate now aggregate only the
target project's sessions (derived from changes[0].path, separators
normalized before dirname), matching the per-project baseline captured at
apply time instead of comparing against all projects.
- baseline.candidateModel labels the candidate explicitly, with a
backward-compatible fallback to metrics key order for existing journals.
- measured model-default rows render a correlation marker instead of a
formatTokens(0) token claim.
- zero-matching-projects now reports an honest project-not-found note; clean
rows route through confidenceFor like every other kind.
- tests: tripwire fires on a same-project regression that global aggregation
would mask (fails on pre-fix code), clean and not-measurable cases,
Windows-separator journal paths with an excluded masking project.
The Codex tab's Plan section reused two Claude-hardcoded views:
PlanLoadingView always showed "Reading Claude credentials..." and
PlanFailedView's Retry always called store.refreshSubscription(), so a
Codex user retried the wrong provider.
Extends the 61209bd parameterization to the two remaining shared views:
- PlanLoadingView(message:) - Claude sites keep the current copy, Codex
sites show "Reading Codex CLI credentials..."
- PlanFailedView(error:onRetry:) - drops the internal AppStore refresh
dependency; Claude sites pass refreshSubscription(), Codex sites pass
refreshCodex()
The failed-state accent stays Theme.brandAccent, matching every sibling
plan view and the section's single-palette convention.
The per-provider ingest list rendered a dozen stacked text rows on
machines with many detected providers. Now one status line with live
tabular-numeral counts (Indexing Claude · 240/699), a single strip of
15px provider logos (dim pending, pulse active, settled done), and a
muted one-time-scan note. ~3 lines of vertical footprint, 340px max
width; reduced motion drops the pulse; warm launches never see it.
Root cause of the field-reported 45s timeout + perpetual slowness: on
a cold cache all six sections spawned different CLI subcommands at
once, each running its own full-history parse (~11-31s, ~3GB RSS),
contending past the 45s kill so the rebuilt cache never persisted and
every poll restarted from zero.
- the first overview fetch runs as a warmup: 10-minute timeout,
re-arms until it succeeds, reverts to 45s after; section polls gate
on that first resolution (usePolled gains enabled), so cold
hydration happens exactly once
- warmup streams the CLI's progress protocol; the splash shows 'First
run: indexing your usage history' with a per-provider ingest list
(logo, live counts, check on done), static under reduced motion,
generic fallback without events; warm launches unchanged
- overview spawns pass --no-timeline (desktop never renders it)
Measured on real data: cold 31s once (was: killed at 45s forever),
warm 2.6s. 295/295 app, 1797 root.
- CODEBURN_PROGRESS=1 emits newline-JSON progress on stderr (provider
list, per-provider start/done, per-file ticks) for GUI first-run
loaders; plain CLI output untouched
- parseAllSessions saves the session cache atomically every 5s during
hydration and after each provider group, so an interrupted cold scan
leaves a warm partial cache instead of nothing
- status --no-timeline skips the granular timeline computation
(~300-450ms per call) for clients that never render it; default
unchanged for the menubar
* fix(report): surface estimated costs distinctly
Providers that estimate tokens or price (kiro, cursor, warp, copilot,
grok, hermes, codewhale, and the codex proxy path) set costIsEstimated
on their parsed calls, but the flag died at the parser boundary: it was
never carried onto ParsedApiCall or into the session aggregates, so a
figure whose tokens were synthesized from content length rendered with
the same authority as a metered one.
Plumb the truth through, mirroring the savingsUSD/isLocalSavings pattern:
a call-level boolean (ParsedApiCall.isEstimated, CachedCall.isEstimated,
persisted so it survives the session-cache round trip) and an additive
aggregate amount (estimatedCostUSD on the model breakdown, session, and
project totals, plus PeriodData and the menubar payload). The amount is
carried rather than a bare boolean so a row that is mostly metered with a
small estimated slice is not indistinguishable from a fully guessed one.
Display: report (TUI) and overview per-model rows prefix the cost with a
tilde and print one legend line; the MCP tables carry the same marker and
legend, and the machine surfaces (report --json, MCP get_usage, menubar /
web payload) expose estimatedCostUSD. Totals math is unchanged; the flag
is display/metadata only.
Bump PROVIDER_PARSE_VERSIONS for every provider that sets the flag so
already-cached sessions reparse once and pick it up. Copilot is excluded:
it is a durable provider, so changing its env fingerprint would discard
OTel cache entries whose source rows may already be pruned.
Also fix the cross-provider project merge, which summed totalCostUSD but
dropped merged-in projects' totalEstimatedCostUSD, undercounting the
project/period estimated total (the same latent gap still affects
totalSavingsUSD, left untouched here).
* test(parser): pin estimated dollars through the cross-provider merge
The merge fix for dropped totalEstimatedCostUSD was not covered: deleting
the summing line left every test green. Extract the merge into an exported
mergeProjectsByCrossProviderKey (no behavior change) and pin both the
measured-plus-estimated and both-estimated merge cases.
* docs(parser): honest merge-comment scope and load-bearing overwrite note
Re-review nits: the merge doc claimed all additive totals are summed there
while totalSavingsUSD still is not (pre-existing gap, tracked separately)
and totalProxiedCostUSD is re-derived post-merge; say so. Mark the
buildPeriodData overwrite in usage-aggregator as load-bearing for the
estimated marker so nobody optimizes it away trusting the daily cache.