Under a saturated full-suite run, fd/CPU starvation can make the
takeover-guard fs ops report unavailable, which the fence correctly
treats as fail-closed; the 1ms-heartbeat stress test misread that as the
serialization race. Retries rescue environmental noise only: the real
race fails roughly 6 percent per verify across 300 verifies, so a
mutated build cannot pass any attempt.
The incremental-append path groups the appended region before the file's
own cached keys join the dedup set, so a streamed assistant id restated
across the append boundary counted twice, and the boundary merge could
splice it into the wrong turn (reproduced on real image-heavy sessions:
one id streamed across several records over seconds, 111 calls vs the
110 a full re-parse yields). Suppressing the restatement instead would
freeze the stale first emission, so neither shortcut matches a full
re-parse. On any id overlap between the appended entries and the cached
turns the shortcut is abandoned and the file re-parses from byte 0.
Rare (about 0.3 percent of real files) and bounded to one file per
refresh. Regression test uses the cold-reparse oracle; mutation-verified
against the unguarded code.
Co-authored-by: reviewer <review@local>
* cache: strict cross-process gate for the warm session-cache refresh transaction (#645)
* lock: serialize the fence against the owner's own heartbeat
verifyStillOwner and the heartbeat tick both take the takeover guard;
without in-process serialization the fence could observe its own
heartbeat's guard file, read it as displacement, and abort a legitimate
publication. Fail-safe, but it discarded the parse the lock exists to
protect (~6% of verifies at a 1ms heartbeat in the repro). Owner-side
guard operations now run through one serializer; cross-process guard
semantics are unchanged. Regression test at heartbeatMs 1, mutation-
verified against the unserialized code.
---------
Co-authored-by: Aditya Vikram Singh <247195684+avs-io@users.noreply.github.com>
Co-authored-by: reviewer <review@local>
* daily-cache: coverage-gated carry on re-derive + single-slice turn attribution
P2: a tz-change re-derive no longer double-counts turns whose day bucket
moved across midnight — secondary slices are carried only outside the
provider's fresh coverage (floor widened by one day on tzChanged).
P3: a turn's editTurns/oneShotTurns/category counts are attributed to the
turn's primary provider slice only, so slice sums equal day-level counts
and partial carries cannot inflate day totals.
* daily-cache: document coverage-widening and rebuild-projects limits; cover empty-call turns
Review addendum: the one-day tz widening assumes an offset delta of at
most 24h (date-line crossings are a known limit); day-level projects of
skinny kept slices are dropped by the coverage rebuild by design. Adds a
regression test pinning the existing skip of empty-call turns.
* split: keep single-slice turn attribution, drop the coverage floor
The P2 coverage floor is reverted pending redesign: on a forced tz
re-derive against a real cache copy it dropped a genuine, non-duplicated
day (claude 2026-06-18, 399.70 USD, 1572 calls) that exists nowhere
else, and the floor also applied to every non-tz re-derive where the
same class of drop is latent. Trading a bounded midnight double-count
for unbounded history loss inverts the never-lose priority; the surgical
alternative (drop only the migrated turn's old-day contribution) is
tracked separately.
P3 stands as authored: turn-level stats attribute to exactly one slice
(majority-calls provider, tie broken by first call), restoring
slice-sums equal day-counts on fresh builds, with its regression tests.
---------
Co-authored-by: reviewer <review@local>
Three review leftovers plus one inconsistency found while fixing them.
computePace returns undefined for a non-finite usedFraction instead of
letting NaN sail through the clamp into every pace field.
Daily-cache retention goes from 730 days to 3650: the cache is the only
durable record of carried days and lifetime now reads from it, so the
2-year prune would have replayed the lost-history bug in slow motion at
that horizon. Measured cost stays small (~2.3 MB / ~11 ms parse per 730
dense days).
The vercel-gateway dashboard-range test pins a relative day instead of
2026-06-01, which would have started failing once the rolling 6-month
window moved past it around December.
Five verified findings from the #756 review, each with a regression test:
Corrections now count only follow-up prompts. The session-opening prompt
cannot be correcting this assistant, however correction-shaped its task
text reads, and the revert pattern no longer matches ordinary 'revert
the <noun>' requests (undo was already guarded; the doc claimed both
were).
Time-to-first-edit returns null when the first edit's timestamp is
unparseable instead of silently measuring to the first parseable one.
File churn normalizes backslashed paths before keying, relativizing, and
basenaming. Previously the payload's basename-only privacy redaction was
a no-op on Windows and shipped the full absolute path, username
included; mixed separators also split one file across churn entries.
Pricing coverage excludes deliberately-free models (local-looking,
local-savings mapped, zero-rate overrides) from the denominator via the
same exclusions findUnpricedModels applies, so a mostly-ollama user no
longer reads high coverage while every cost-bearing call is unpriced.
An absent coverage value now reaches the payload as null, never as a
fabricated 100%.
Also shares the one-shot minimum-edit-turns threshold between
worstOneShotCategory and the coaching gate so they cannot drift.
* feat(insights): workflow intelligence (corrections, time-to-first-edit, file churn, coverage) + dash wire-up
* menubar: propagate workflow-intelligence fields onto the cache-backed headline
The carry-forward headline (#755) serves totals from the durable day set
and merges only session-derived fields from the scan. The new workflow,
topReworkedFiles, and pricingCoverage fields were computed by the scan
but never propagated, so they silently defaulted on the all-provider
path (pricingCoverage rendered a dishonest 1.0). Propagate them like the
other scan-only fields and pin it with a carried-headline regression
test.
Claude Code deletes session transcripts after cleanupPeriodDays (default
30) at startup. Daily totals survive in the daily cache, but per-session
detail older than the cutoff is unrecoverable, and most users learn this
only after losing months of history. Doctor now reports the effective
retention and, below one year, warns with the exact settings.json edit
that preserves transcripts.
Re-apply the CLI/dashboard Lifetime split from #391/#416 on current main.
- add lifetime as a first-class period (all stays last 6 months)
- wire period through cli-date, dashboard key 6, and CLI commands
- refresh cli-date and cli-json-daily regression coverage
Part of #389. Supersedes #416.
Reuse one concrete six-month scan for interactive history and selected-period totals. This restores network-backed providers, removes concurrent refresh scans, and keeps non-interactive reports scoped to their requested period.
* 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>
Follow-up to #719, closing the two deltas flagged in the #714 discussion:
- fs.realpathSync preserves the caller's path casing on case-insensitive
filesystems (macOS APFS, Windows), so the same repo reached through
differently cased paths produced distinct canonical keys and split into
separate groups. fs.realpathSync.native returns the on-disk casing, making
the canonical key truly canonical. Regression test creates a real repo,
references it through altered casing (guarded to case-insensitive
filesystems), and asserts one group.
- Documents the accepted residual: a main repo moved on disk after
'git worktree add' leaves git's linked-worktree metadata stale, so its
worktrees may not unify; that is git-state staleness, not a grouping bug.
Adds a provider for Kimi Code CLI (MoonshotAI/kimi-code, the successor of
kimi-cli), reading ~/.kimi-code/sessions/wd_*/session_*/: state.json for
session metadata and per-agent agents/<id>/wire.jsonl event streams.
- Usage from usage.record events: inputOther->input, output->output,
inputCacheRead->cacheRead, inputCacheCreation->cacheWrite. The store has
no cost fields, so cost is computed from tokens and flagged estimated.
- Real model attribution: usage.record.model carries the config ALIAS; the
real model id lives on llm.request events. Rows resolve the alias through
the alias->model map first, with the nearest preceding request as
fallback, so aliases never leak into reports.
- Subagent wire files parse as separate sources under one session without
double counting; multi-turn continuations in one wire.jsonl are one
session with multiple turns; retry-only failed sessions parse to zero
usage; malformed JSONL lines are skipped.
- Tool calls feed the tool breakdown; tool state resets at turn boundaries
so a failed turn cannot bleed its tools into the next row.
- KIMI_CODE_HOME override, eager registration, PROVIDER_ENV_VARS and
PROVIDER_PARSE_VERSIONS entries, probeRoots for doctor, docs page.
Skill attribution was gated on the turn classifying as 'general', but
classifyByToolPattern checks hasSkill last, so any turn invoking the Skill
tool alongside Read/Bash/Edit/MCP got a real category and its skill was
silently dropped. Skill turns virtually always carry other tools, which is
why skills came back empty in all 25 usage snapshots (#741) while mcpServers
populated.
subCategory is now set for any turn with Skill tool_use; category assignment
is untouched. The claude parse version is bumped so cached session summaries
(which lack skill attribution) re-parse on upgrade.
Adds an eager provider for Amazon Quick Desktop (aws.amazon.com/quick/desktop),
which stores local data under ~/.quickwork. Usage source of truth is the
per-day EMF metrics JSONL (Model, InputTokens, OutputTokens, CostUSD,
session_id); real CostUSD passes through the cache like kiro/devin/hermes.
sessions.db (SQLite, read-only) enriches calls with titles, first user
message, and tool names, and provides estimated usage (quickdesk-auto,
costIsEstimated) for sessions that predate metrics coverage. Multi-profile
layout via profiles.json entries plus the migrated legacy root; honors
QUICKWORK_HOME; schema-tolerant (sqlite_master and PRAGMA introspection,
per-line JSONL error isolation). The on-disk schema is reverse-engineered and
documented as such in docs/providers/quickdesk.md.
* 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.
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.
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.
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.
- 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.
* feat(doctor): add per-provider detection diagnostics command
When a provider reports zero (or a number that looks wrong) there was no way
to see why: whether the tool is not installed, an env override points at the
wrong directory, the data dir is empty, or parsing failed. Users had to file
issues like "OpenCode does not work" with nothing to go on.
`codeburn doctor` closes that gap. For every provider (or one via --provider)
it shows the exact directories/dbs probed with any env override and whether the
path exists, how many session files were discovered, how many of a bounded
sample parsed cleanly, the cached file count and parser version, and a one-line
verdict: OK (n sessions), NOTHING FOUND with a concrete likely cause, or
ERRORS (n parse failures). Output is a human table by default or --json.
The collect logic is a pure function separated from rendering so tests exercise
it without a TTY. It runs fully offline and read-only (never writes caches or
config, and skips the one network provider's parse), and isolates each provider
in its own try/catch so a single thrower becomes an error row instead of
crashing the report. Providers expose their scan roots via an optional
probeRoots() so the exact paths are shown even when zero sessions are found,
reusing each provider's own path resolution rather than duplicating it.
* fix(doctor): make the inert promise actually true
Adversarial re-review reproduced two false guarantees. Cursor's parser
writes its results cache to disk before its first yield, so a doctor run
created ~/.cache/codeburn/cursor-results.json on a clean machine; the
collect pass now sets CODEBURN_SUPPRESS_CACHE_WRITES (restored after) and
the cursor writer honors it. Antigravity's parse probes for a live
language server (spawns ps and lsof, RPCs the IDE when found); doctor now
skips its parse sample the way network providers are skipped, keeping
discovery counts meaningful.
Also stop blaming CODEBURN_CACHE_DIR in NOTHING FOUND hints (it is our
cache location, not a discovery path) and correct the sample-cap comment:
the cap truncates yields, eager parsers still do whole-file work.
* fix(copilot): bill CLI input and cache tokens from session.shutdown
Copilot CLI sessions (~/.copilot/session-state/*/events.jsonl) recorded
only outputTokens, because the parser read assistant.message events and
ignored the session.shutdown rollup. That rollup is the sole on-disk
source of a CLI session's input, cache-read and cache-write tokens, so
cost was underreported (in the reported case 35k+ cache-read tokens were
dropped entirely).
Handle session.shutdown in the CLI (non-transcript) JSONL path and emit
one supplementary call per model from modelMetrics.<model>.usage. The
per-turn assistant.message events keep owning output tokens, tools and
turn structure; the shutdown call contributes only the input and cache
tokens they lack, with output excluded, so no dimension double-counts and
the combined cost equals the full CLI-measured cost. usage.inputTokens is
cache-inclusive (input + cache_read + cache_write), so the cache
components are subtracted to recover the uncached input calculateCost
expects. These are real counts written by the CLI, so costIsEstimated is
false. Sessions without session.shutdown (crashed or still running) keep
the exact previous output-only behavior, and the VS Code, JetBrains and
OTel paths are untouched.
Bump the copilot entry in PROVIDER_PARSE_VERSIONS so already-cached CLI
sessions re-parse and pick up the corrected tokens and cost.
* fix(copilot): never emit an empty-timestamp shutdown call
A session.shutdown without its own timestamp and without sessionStartTime
produced a call with an empty timestamp, which the date-range filters in
parser.ts drop silently, erasing exactly the tokens this feature bills.
Fall back to the last stamped event in the session.
* fix(cache): durable orphans survive provider fingerprint changes
Bumping a durable provider's PROVIDER_PARSE_VERSIONS changed its env
fingerprint, and getOrCreateProviderSection replaced the whole section on
mismatch. For copilot OTel orphans the discarded cache entry is the only
remaining record of pruned spans, so the first post-upgrade run silently
and permanently erased month-to-date history. Carry forward exactly the
entries whose source no longer exists; present sources still drop and
re-parse under the new fingerprint, which is the point of a bump.
Regression test forges a pre-upgrade fingerprint on a pruned-source
orphan and asserts it stays counted and cached across the transition.
Resolves menubar-json conflicts: main's granular-history timeline
param unions cleanly with this branch's providerDetails and currency
payload additions; both new tests kept.
* feat(antigravity): add support for Antigravity IDE storage on Windows
CodeBurn previously only detected Antigravity CLI usage (.pb files under
.gemini/antigravity/). Antigravity IDE on Windows stores session state in
VSCode-style storage at %APPDATA%\Antigravity IDE\User\globalStorage\state.vscdb,
which was not detected.
Add support for reading Antigravity IDE sessions from the VSCode-style storage:
- Extend CONVERSATION_ROOTS to include APPDATA Antigravity IDE path
- Refine path classification to properly identify IDE vs CLI sessions
- Handle missing per-call timestamps by stamping file mtime as fallback
- Bump CACHE_VERSION to 4 for cache invalidation
Fixes: CodeBurn reports zero usage when Antigravity IDE is actively used.
* fix(antigravity): stamp mtime fallback at emission, tighten path classification
- Apply the mtime fallback to a copy at emission instead of mutating and
persisting it into the cache, so a later file rewrite can't retro-date a
session's history to the new mtime. SQLite gen_metadata rows have no real
per-call time; the RPC path still uses chatStartMetadata.createdAt.
- Drop the unreachable APPDATA classifier branch (discovery only walks the
~/.gemini roots; APPDATA state.vscdb holds no token usage). This also removes
the misroute of base-antigravity paths under an "Antigravity IDE" profile dir.
- Bump CACHE_VERSION to invalidate caches that persisted a synthesized mtime.
* fix(antigravity): stabilize untimestamped call times across DB rewrites
Extract ChatStartMetadata.created_at from proto-encoded data and decode multiple timestamp formats (ISO string, Timestamp submessage, unix varint). Implement assignStableTimestamps() to preserve first-seen timestamps across file rewrites, preventing retro-dating of sessions when .db files are modified. Make conversation roots dynamic (computed per call) to honor environment overrides in tests. Add comprehensive timestamp stability tests verifying that timestamps remain fixed across file mtime changes while respecting date-range filters.
* test(antigravity): assert today range excludes first-seen timestamps
Adds a test case to verify that the 'today' date range filter correctly excludes sessions based on their first-seen timestamp, not file modification time. This ensures that even if a database file is rewritten with a later mtime, sessions with earlier first-seen dates are properly filtered out.
Refs #411
* feat(claude): account for /advisor escalations under the advisor model
Claude Code's advisor tool (/advisor) escalates hard decisions mid-turn
to a stronger model. Those tokens are recorded inside
message.usage.iterations as advisor_message entries with their own model
and are excluded from the top-level message.usage totals. The parser read
only the top-level totals, so advisor spend was silently dropped and never
attributed to the advisor model.
Emit a separate call per advisor_message iteration priced under the
advisor model, preserve those iterations through entry compaction and the
large-line (>32KB) byte-scanner path, and fold advisor cost into the guard
budget. Bump the claude parser and guard cache versions so existing caches
re-parse and pick up the advisor spend.
* test(guard): pin advisor cost fold in the live budget
The guard's advisor fold in computeSessionUsage had no test; dropping
advisorCost from the line cost left all 30 guard tests green. Pins the
fold (advisor line costs more than the same line without iterations)
and the per-message replace semantics under streaming rewrites.