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.
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.
* feat(dashboard): add inline daily history paging
* refactor(dashboard): make daily activity directly scrollable
* fix(dashboard): keep the empty state for users with no history at all
Scrollable mode kept the dashboard shell up whenever full history exists,
but a truly-new user with zero data anywhere saw a zeroed skeleton instead
of the No usage data message. Extract the decision into showEmptyState so
the boundary is test-pinned: empty period with history renders the shell,
empty everything renders the empty state, and the loading pass never
flashes it.
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
* feat(dashboard): add granular usage timelines
* fix(dashboard): readable timeline legend and month-scale buckets
Session legend labels now use the real projectPath's last two segments
(app/web) instead of the sanitized project dir, which rendered as an
unreadable -Users-name-... dump and truncated in the legend.
Hourly buckets now cap at 8 days; longer ranges bucket daily. A 30-day
window rendered 720 overlapping hourly spikes, unreadable as a chart.
The browser no longer gives the backend session_other/model_other
aggregate a top-N slot, which rendered two identical Other legend
entries whenever the backend fold out-ranked a real series.
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
* fix(claude): use APPDATA for Windows Desktop sessions
* test(claude): harden withPlatform mock against cross-file leak
The process.platform mock omitted configurable:true and never restored
the value when the property had no own descriptor, leaving win32 mocked
for later tests on the same Vitest worker. Add configurable + an else
branch that deletes the override to expose the real inherited value.
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
A project's first/bootstrap session is a different kind of work (one-off
scaffolding), not an expensive instance of routine work. While a project is
still young its peer sample is thin, so the leave-one-out average is dominated
by a few routine sessions and the founding session's legitimately high one-off
cost trips the >2x outlier multiplier. Exclude each young project's earliest
costed session from the outlier finding only; mature projects are unaffected so
genuine outliers still surface. Young = fewer than 2x MIN_SESSIONS_FOR_OUTLIER
costed sessions.
Fixes#664
yield always analyzed all providers, so provider-filtered GUI panels
(cost per outcome, reverts, abandoned) showed all-provider numbers
under a filtered header.
ProviderCost now keeps the internal id alongside the display name. The
providers map keys stay lowercased display names (byte-identical, the
Swift menubar decodes them); new current.providerDetails exposes
{id,label,cost} so GUI clients can round-trip a valid --provider value.
New top-level currency {code,symbol,rate}: payload values remain raw
USD, clients convert at display time like the Swift menubar does.
Compare defined cache-hit as read/(input+read+write) while menubar-json
and every other surface use read/(input+read), so the same model showed
two different numbers depending on screen. Standardize on the latter;
TUI compare output changes intentionally.
The desktop app's getOptimizeReport handler already appends --from/--to
when a custom range is active; without these options commander rejects
the invocation and the Optimize screen errors under a custom range.
v1 modern execution files carry the same metered credits as v2
(usageSummary[].usage, unit "credit" — the predecessor of v2's
promptTurnSummaries), but the parser only harvested usedTools from
that array and priced executions from estimated tokens. Since v2 only
ships in brand-new IDE builds, v1 is the format nearly all Kiro IDE
users are on today. Sum usage across usageSummary entries and price at
the public overage rate.
Align all three credit parsers (CLI, v1, v2) on one fallback contract —
gate on summed credits > 0 and fall back to token-estimated pricing
with costIsEstimated: true:
- CLI turns without metering_usage were priced at a frozen $0
- CLI turns with an EMPTY metering_usage array (75 of 10,105 real turn
metadatas — meta written before metering lands) passed the truthy
presence check and froze $0 marked as real cost
- legacy .chat calls now carry costIsEstimated: true, which was always
the reality but never stated
Validated against a real machine: 221 of 277 v1/legacy calls now carry
metered cost ($20.11) instead of token estimates that had overstated
them ~5x; verified usageSummary values are per-turn amounts, not
cumulative counters (sequences fluctuate). Call counts unchanged before
and after — nothing gained, lost, or double-counted.
Also document the companion-file fingerprint blind spot at
fingerprintFile: kiro CLI credits and v2 modelId live in companion
files the single-file fingerprint never sees, so a parse racing the
companion write can cache fallback values that only self-heal while
the transcript keeps changing.
AI-Origin: ai-generated
AI-Tool: kiro
providerCallToCachedCall drops provider-computed costUSD unless the
provider is allowlisted, and cachedCallToApiCall then re-prices from
token counts. Kiro's cost now comes from metered credits, which token
pricing cannot reproduce — reports were understating real kiro spend
~18x (e.g. $130.72 of metered CLI credits reported as $7.20 of token
estimates). Add kiro to the pass-through allowlist, same as the other
platform-billed providers (mistral-vibe, devin, hermes, ...).
Both persistence layers froze pre-fix dollar values and neither
self-invalidates (historical session files never change), so bump:
- session cache: CACHE_VERSION 4 -> 5 (cached kiro entries carry
costUSD: undefined and would keep being token re-priced forever)
- daily cache: DAILY_CACHE_VERSION 10 -> 11 with MIN_SUPPORTED_VERSION
raised (finalized days carry token-estimated kiro costs off by up to
16x per model), same as the v10 cursor precedent
Test: end-to-end through parseAllSessions — a CLI fixture with 2.5
metered credits must report $0.10; fails with the token re-price
($0.000048) when the allowlist entry is removed.
Docs: document the pricing contract in docs/providers/kiro.md — metered
credit cost is frozen at parse time, so price-override / model-alias
do not affect kiro dollar amounts (shared tradeoff of all allowlisted
providers).
AI-Origin: ai-generated
AI-Tool: kiro
Kiro's new IDE builds write sessions to ~/.kiro/sessions/<hash>/sess_<id>/
(session.json + messages.jsonl event log) instead of globalStorage. Add a
v2 parser and discovery, plus fixes surfaced while validating against a
real upgraded machine:
- Parse v2 event-sourced turns (user/turn_start/assistant/tool_call/
tool_result/usage_summary/turn_end) with per-execution dedup keys
(kiro-v2:<session>:<execId>) and defensive flushes for out-of-order
events and in-progress sessions
- Price turns from real metered credits at the public overage rate
(USD_PER_KIRO_CREDIT = $0.04/credit, individual plan: $20/mo for
1,000 credits), falling back to token estimation only when a turn has
no usage_summary (aborted/in-flight); costIsEstimated distinguishes
the two
- Fix pre-existing CLI parser bug: metering_usage values are credits,
not dollars — costs were overstated 25x at the real rate
- Count v2 tool_result content as input context (matching the CLI
parser's ToolResults treatment); previously only the user prompt was
counted, undercounting input ~25x on agentic turns
- Keep reasoningTokens disjoint from outputTokens: downstream
aggregation (models-report, audit-report, parser) sums the two
fields, so folding reasoning into output would double-count. Combine
them only for the token-pricing fallback, matching codex/gemini
- Take the real modelId from session.json, so v2 sessions are not
mislabeled kiro-auto
- Extract parseWorkspaceSession from the inline createParser block for
parity with the other four format parsers
- Add v2SessionsRootOverride and stop deriving the v2 scan root from
the cliDir parent when only agentDirOverride is set (tests were
scanning the system tmpdir)
- Tests: v2 parsing/discovery/dedup/routing, credit vs fallback
pricing, tool_result turn scoping, reasoning disjointness, cli-dir
skip guard, and a mixed-format coexistence suite (legacy .chat + v1
executions + workspace-sessions + CLI + v2 on one machine) asserting
exact aggregate counts and no double counting
- Docs: v2 store layout, credit pricing, dedup namespaces, and the
disjoint-store verification (v1->v2 is a clean cutover, no
migration/dual-write observed)
AI-Origin: human
Before-merge items:
1. HTTPS enforced on every remote endpoint (RFC 8252 §8.3): baseUrl,
issuer, authorization/token/revocation endpoints, and the traces
endpoint all reject non-https, with a loopback (127.0.0.1/::1/
localhost) exception for offline tests and local dev. Enforcement is
central (assertHttps) — the browser-open guard is no longer the only
check whose failure was swallowed.
2. Credential store test isolation: CODEBURN_SYNC_TOKEN_STORE=file
forces the file store (honors HOME) so the offline suite never
touches the real macOS login keychain. Set in the e2e suite.
3. Golden pins for deriveSpanId/deriveTraceId/deriveDeviceId with fixed
inputs and expected hex — the idempotency contract depends on these
encodings being stable across releases; a green-tests encoding change
would silently double-count history on span-ID-keyed backends.
getDeviceId refactored over a pure deriveDeviceId(host, user).
Smaller review items:
- Callback server: ready promise resolves the actually-bound port from
the listening event (kills the 100ms-sleep race after port fallback);
Connection: close on all responses + closeAllConnections() on
shutdown (pooled keep-alive sockets from a closed server could
swallow requests aimed at a later server on the same port); error
handler guarded so a post-bind error can never rebind to a different
port than advertised; optional ports param ([0] = ephemeral) removes
fixed-port contention between parallel test workers.
- fetchOidcConfig verifies the issuer claim matches the fetch origin
(OIDC Discovery §4.3 mix-up defense).
- partialSuccess.rejectedSpans wrapped in Number() — proto3 int64 JSON
mapping sends strings from strict protojson servers; += would
concatenate.
- Ledger writes are atomic (temp + rename); corrupt-ledger recovery and
no-tmp-left-behind tests added; XDG_CACHE_HOME honored (ledger is
reconstructible state, not config).
- Mock IdP now implements /oauth2/authorize (registers PKCE challenge,
302s to redirect_uri) and verifies S256 code_verifier + single-use
codes at the token endpoint. The e2e drives the real redirect flow
and asserts wrong-verifier and code-reuse are rejected — PKCE binding
is now exercised end to end.
- sync reset calls clearLedger() instead of reimplementing the path.
- push sets exit code 1 on rate-limited/server-error outcomes so cron
and script callers can detect incomplete pushes.
Deferred (noted for fast-follow): macOS 'security -i' stdin mode
(untestable on this Linux box), ai.cost_estimated as a real
ParsedApiCall flag (touches core parser types).
Sync suite: 81 passing (5x stable), 5 developer-only.
AI-Origin: human
Adds an opt-in `codeburn sync` command group: setup, push, status,
logout, reset. Developers authenticate via standard OIDC browser login
(Authorization Code + PKCE, provider-agnostic — Cognito, Auth0, Okta,
Keycloak, etc.) and push per-call usage metadata to a team-operated
OTLP/HTTP endpoint.
Privacy is structural: the payload builder has no field that can carry
prompts, responses, file contents, or bash commands. Only metadata
leaves the machine (provider, model, tokens, cost, project, tool names,
timestamps). There is deliberately no flag to include more.
Setup derives everything from a single URL:
GET {base}/.well-known/codeburn-export.json
→ issuer, client_id, traces_path, max_batch_size
Modules:
- discovery.ts discovery doc fetch/validation (version-gated)
- auth.ts OIDC discovery, PKCE, localhost callback (:19876-78),
token exchange/refresh/revoke
- credentials.ts refresh token in OS keychain (macOS security / Linux
libsecret / Windows DPAPI / 0600 file fallback). All
subprocess calls use execFileSync arg arrays; tokens
flow via stdin or env var — never shell-interpolated
- config.ts ~/.config/codeburn/sync.json
- ledger.ts client-side sent-ledger for exact dedup (no watermark
races), 6-month prune
- otlp.ts ParsedApiCall[] → ExportTraceServiceRequest JSON with
deterministic IDs: span=SHA256(dedupKey)[:8],
trace=SHA256(sessionId)[:16] — every retry idempotent
- push.ts orchestration with typed outcomes; pushes run to
completion — 429 Retry-After honored (≤120s/wait,
3 retries/batch) before deferring; partial_success is
batch-atomic (nothing ledgered, whole batch retries);
50K safety valve, no routine cap
- cli.ts command layer; --dry-run reports exact counts/cost
without sending
Preview feature: protocol may change between releases.
AI-Origin: human
Adds JSON list mode so the desktop Settings screen can read current
config: `model-alias --list --format json` emits AliasRow[] ({from,to},
sorted by from) and `proxy-path --list --format json` emits string[].
Text output and all mutation behavior are unchanged.
Addresses T1 review: discovery.ts logs mDNS bind errors to stderr and returns
already-found devices instead of masking failures as empty; spend-flow uses a
collision-proof "other" sentinel and a local-date period label; the spend/date
flags get friendly error handling; devices-json tests are deterministic (mocked
discovery) and assert the read-only mutation guards. 13 tests green, tsc clean.
Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
New `codeburn spend --format flow-json` emits the model x project cost matrix
(uncapped, then top-8 each side with an "other" rollup) for the desktop Spend
Sankey. Adds --format json to devices / devices scan / share status / identity,
matching the web /api/* shapes. Read-only; text output unchanged when --format
is omitted. discovery.ts degrades mDNS bind errors to an empty scan result.
Implemented by Codex gpt-5.5 (high); committed by Fable (git was blocked in the Codex sandbox).
extractBashCommands recorded the wrapper (sudo, npx, rtk, and friends)
instead of the command it delegates to, so any agent that prefixes its
shell calls collapsed its whole bash breakdown into one meaningless
bucket and the optimize detectors lost the actual tool.
Skip a known set of command wrappers when a real command follows, and
interleave that skip with the existing VAR=value env-assignment skip so
forms like 'sudo NODE_ENV=prod node x' resolve to the real tool. A
wrapper followed by a flag or a quoted token is kept as-is so we never
emit a garbage key.
Closes#657
Users who run MCP tools through a CLI wrapper (e.g. philschmid/mcp-cli)
instead of registering servers natively don't produce Codex
mcp_tool_call_end events; Codex logs a plain exec_command. So their MCP
usage showed only as a shell command and was absent from the MCP
breakdown, even after #513 fixed the native path.
- Recognize `mcp-cli [options] call <server> <tool>` in the exec command
and also attribute it as mcp__<server>__<tool>. The exec still counts as
Bash since it genuinely is a shell exec. Flag-tolerant, quote/path/bash-lc
tolerant, and scoped to the mcp-cli binary (not foo-mcp-cli); only the
`call` subcommand (a real execution) matches, not info/grep/listing.
- Register `codex` in PROVIDER_PARSE_VERSIONS. session-cache.json serves
unchanged session files without invoking the provider parser, so the
codex-cache version bump alone would never re-attribute already-cached
sessions. This also retroactively repairs #513's native-MCP fix for
users whose files were cached before it shipped.
- Bump CODEX_CACHE_VERSION 4->5 for the provider-internal layer.
Tested: parse cases (bash -lc wrapper, flags-before-call, argv array,
plus info/grep/foo-mcp-cli negatives), and a mutation-verified cache
regression test that fails without the PROVIDER_PARSE_VERSIONS entry.
ReDoS-checked. Reviewed with a Fable 5 adversarial pass (Codex is down);
its two must-fixes (the cache gap and the flags-before-call miss that
would have missed the reporter's own sessions) are folded in.
Fixes#478