* feat(antigravity): align tool parsing, MCP attribution, and activity classification
Extends Antigravity tool classification and MCP normalization:
- Classify manage_task as planning and search_web/read_url_content as exploration in TASK_TOOLS/SEARCH_TOOLS.
- Recognize invoke_subagent in hasAgentSpawn alongside Agent in parser.ts to categorize delegation turns.
- Implement and export normalizeAntigravityToolCall to normalize both eager mcp_<server>_<tool> and lazy call_mcp_tool into canonical mcp__<server>__<tool>.
- Connect toolDisplayName to normalizeAntigravityToolCall.
- Bump ANTIGRAVITY_CACHE_VERSION to 7 and PROVIDER_PARSE_VERSIONS.antigravity to worktree-project-grouping-v7 so existing cached sessions re-parse and gain eager MCP attribution.
- Add comprehensive unit tests in classifier and provider suites.
* refactor(antigravity): map tool names in the provider, not the shared classifier; daily cache bump
---------
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
* Add regressions for request-level Copilot journal usage
* Read request-level input tokens from Copilot journals
* Refresh cached Copilot journal usage and settled days
* Use local date keys in the journal cache regression
* feat(quota): Devin plan quota from the Devin CLI's local cache
Reads the newest ~/.cache/devin/cli/user_status.*.bin and walks only the
plan status message of its protobuf payload: plan name, daily and weekly
remaining percent and their resets. No network call and no API key; each
reading says how old the cache is. Any out-of-range field fails the read
instead of showing a number, and a window whose reset has passed since
the fetch is dropped.
CLI reader feeds codeburn quota and the Windows dock; the Mac Capacity
Dock gets the same decoder, and Devin moves from browser session to
local CLI, live.
* docs(windows): count twelve quota readers
* fix(app): list Devin among menu bar quota providers
* feat(devin): read usage from sessions.db
Devin keeps every request in sessions.db message_nodes; the transcript
exports hold only a few sessions. Usage now comes from the store, one
source per non-hidden session, read in node_id keyset batches with only
the needed JSON fields pulled. Sibling retries share a request_id and
dedup on it; off-main-chain requests count. SQLITE_BUSY is rethrown so
the refresh retries. Transcripts are read only when the database is
missing or has no message_nodes, so the two never double count.
The legacy metadata.metrics path no longer carves cache reads out of an
input_tokens count that never included them.
sessions.db sources rely on the WAL-folded fingerprint instead of a
forced reparse. Parse version sessions-db-v1; daily cache v43 with devin
in PENDING_REDERIVE, since transcript-era days dated steps on the
session's last activity.
* fix(devin): take only text user content as the prompt
* test(daily-cache): include devin in pending re-derive expectations
* test(mac): clean up scratch UserDefaults suites, guard against new leaks
* test(mac): fixed suite names so late cfprefsd write-backs reuse one file per test
* test(mac): start every scratch suite clean
* fix(amp): update stale ledger comment; split AMP_DATA_DIR list in the Windows guard
* fix(amp): watch thread subfolders under each Amp root in the Windows guard
* fix(zcode): attribute PR-linked spend by reading user prompts from the transcript tables
The ZCode provider emitted every call with an empty userMessage and no
prLinks, so the shared attribution layer had nothing to correlate and
ZCode sessions never appeared under attributed pull requests - even
though the references sit right there in the same SQLite database: a
user message's anchor.turnId matches model_usage.turn_id, and its text
(metadata.inputIntent.text, else the message's text parts) carries the
GitHub PR URLs.
Read the message/part tables per session and emit per-turn prompt text
plus the session-wide PR URL set. The parser layer then extracts
per-turn prRefs from userMessage and seeds the cached file's
session-level prLinks, exactly like every other provider. A DB without
the transcript tables degrades to the previous behavior.
Parse version bumped (pr-attribution-v1) so cached ZCode sessions
re-parse once and gain the fields.
* fix(zcode): rethrow busy db, read only user parts, daily cache v45
---------
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
* Add regression coverage for Claude queued prompts
* Count queued Claude human prompts in turn grouping
* Cover queued prompts through Claude session cache
* Reparse cached Claude turns for queued prompts
* Refresh daily categories after Claude turn regrouping
* fix(claude): count only human queued prompts, skip peer agent messages
---------
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
* feat(menubar): Cost/Tokens toggle on the Trend chart, Cost by default
Bar height tracked tokens, so a cheap high-token day looked bigger than an
expensive low-token one. The choice is remembered across launches.
* test(menubar): cover the trend Cost/Tokens toggle default and fallback
---------
Co-authored-by: Jay Johnston <750504+jayjohnston@users.noreply.github.com>
* feat: Amp support - read the local thread mirror (#1572)
Amp (Sourcegraph's coding agent) mirrors every thread it serves as one
JSON document per thread at ~/.local/share/amp/threads/ (AMP_DATA_DIR
honored, comma-separated). Each file is { id, messages,
usageLedger?{events} }; the ledger's events are the per-request record
(tokens.input/output, cache tokens joined from the billed message
through toMessageId -> messages[].messageId), with a per-assistant-
message usage fallback for threads without a ledger and a total-only
record billing as output.
Amp bills a subscription and records no charged dollars, so every call
is priced from the underlying model's token rates and marked
costIsEstimated; the recorded credits field is ignored (no published
per-credit dollar rate). Thread files carry no cwd, so sessions group
under the provider name. Registered eager; parse version
usage-ledger-v1; no artwork yet (mono-initial fallback, like Codebuff).
Surfaces: CLI registry + doctor probe roots, Electron sandbox read
paths + menubar catalog (Amp after Cursor, matching the Swift order),
Windows tray tab color + usage_guard root, GNOME prefs dropdown, macOS
ProviderConnectionCatalog, docs/providers/amp.md, README tool list,
CHANGELOG.
* fix: address Amp provider review feedback
serde's rename_all on an internally tagged enum only renames the
variant tag, not the fields of a struct-like variant, so this field
shipped as claude_profiles. The frontend reads answer.claudeProfiles,
so the Claude profiles settings row and the per-config dock rings
from #1570 never showed.
Adds a field-level rename plus a test pinning the serialized key.
* fix(pricing): restore the DeepSeek v4 and gpt-5.6-codex repricing invariants (#1134)
The 0.9.21 snapshot caught the 2026-08-24 repricings mid-transition and
relaxed two tests/models.test.ts invariants. Upstream has since synced:
- DeepSeek v4: LiteLLM now carries the official peak rates for both the
bare and the deepseek/-namespaced rows (pro 1.32/3.96 per million,
flash 0.30/1.20), so the stale hand-pinned MANUAL_ENTRIES (from before
PR #27056 merged) are removed and prefixed-equals-bare is asserted
again.
- gpt-5.6-codex / -max: still absent from LiteLLM, so their
MANUAL_ENTRIES remain but now mirror the repriced gpt-5.6 base row
verbatim (4/20 per million, tier block included). codex-equals-base
is asserted again, and the long-context tier test now expects the
codex SKUs to carry the base tier.
The refresh also exposed a pass-2 ordering defect in
scripts/bundle-litellm.mjs: a reseller row like
openrouter/deepseek/deepseek-v4-pro could claim the empty
deepseek/deepseek-v4-pro slot before the official row was reached (the
winner was decided purely by upstream JSON key order), pricing the
namespaced id at the resale rate. A prefixed row may now claim only a
stripped slot that is not itself an upstream entry name; multi-segment
ids that exist only as reseller rows keep resolving as before.
Bundled snapshot regenerated: 24 rows moved by upstream drift
(claude-sonnet-5-5 family added, deepseek-v4.1-flash repriced,
zai/glm-4.6 cache-write filled), plus the deepseek/codex corrections.
Token-priced calls re-price on read; no re-parse is needed.
* fix(pricing): repin cline-cli parse version and cover #1134 shadow bug
- session-cache.ts: bump the cline-cli parse version so cached estimated
costs (e.g. offline DeepSeek v4.1 flash) re-price on read, since
cline-cli is in REPORTED_COST_PROVIDERS.
- CHANGELOG.md: note the bundler fix also reprices zai/glm-4.6 from the
vercel_ai_gateway resale row to the official row, and moves the two
mistral embed models to the fallback table.
- bundle-litellm.test.ts: add a fixture covering a reseller-prefixed row
that sorts before the official row it would otherwise shadow.
---------
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
* fix(openclaw): read the per-agent sqlite transcript store the 2026-09-01 migration writes
OpenClaw 2026.8.1 moved sessions from agents/<agent>/sessions/*.jsonl to
agents/<agent>/agent/openclaw-agent.sqlite, so discovery resolved to zero
sources and doctor reported the provider empty (#1259).
transcript_events keeps each JSONL envelope verbatim in event_json, so the
JSONL event reducer is extracted and shared: both eras parse, dedup and
cost identically, and migrated sessions keep their original ids. Sessions
are listed per store and parsed in keyset batches of 2000 rows so a
gigabyte store never loads whole. agent-schema 23 lets a row carry its
event as an event_zstd BLOB instead; those decode on Node 22.15+ and skip
with one notice below that.
Fixes#1259
* test(openclaw): batch fixture inserts in a transaction; hash id-less events before the created_at fallback
---------
Co-authored-by: ozymandiashh <ozymandiashh@users.noreply.github.com>
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
* fix(pricing): carry long-context tier rates through bundling and price them where billing evidence exists
LiteLLM publishes *_above_<n>k_tokens rates for 418 models - OpenAI's
gpt-5.4/5.5/5.6 family above 272k, Anthropic above 200k, and 128k/256k/
512k tiers elsewhere - but the bundler flattened every entry to the
fixed five-slot tuple and calculateCost had no threshold dimension, so
a request past its threshold priced at the base rate forever (#1076).
The tuple gains an optional sixth slot {threshold, input, output,
cacheWrite, cacheRead}, the threshold parsed from the key suffix
because LiteLLM ships no numeric field (272k means 272,000; #1075
verified assuming 128k fabricates +64% spend). Old bundles parse
unchanged; CACHE_SCHEMA_VERSION moves to 5 (4 is taken on main by the
implicit cache-write billing) so a stale on-disk live-pricing cache
cannot reintroduce the old behavior; the live-fetch parser picks the
same tier up; service-tier _priority/_flex variants and the 1-hour
cache-write combination are not context thresholds and stay out.
A tier applies when the request's prompt tokens (input + cached input,
the existing grok-4.6 definition) reach the threshold, each published
rate replacing its base and omitted slots keeping the base.
Application is gated on billing evidence via TIERED_PRICING_PROVIDERS:
codex today, where the maintainer's corpus measurement shows the
surcharge passing through; a real Copilot session on gpt-5.6-terra
with ~6M-token prompts billed at the BASE rate, so Copilot keeps base
pricing until such evidence exists - applying it there would fabricate
spend, the exact class #1075 warned about. The audit report recomputes
through the same tier swap so recomputedTotalUSD keeps tracking
attributedCostUSD.
* fix(pricing): read tier rates only from the chosen threshold; e2e codex tier test
Repair pass from review: both tierOf implementations now collect rates
per threshold and read only from the largest one, so a hypothetical
two-tier entry can never mix the smaller tier's rates under the bigger
threshold; the bundler validates tier values exactly like the live
parser (finite, non-negative). A codex end-to-end test drives a real
rollout past gpt-5.5's 272k prompt threshold and asserts the tiered
arithmetic explicitly (a typo'd provider string in codex.ts would
leave the call at base rates and fail it), alongside the no-provider
base-rate control that keeps the real Copilot billing pin intact.
Comment fixes: the priceOverride wording, the grok pin's actual
resolution branch, and the threading note on TIERED_PRICING_PROVIDERS
for whoever adds the next provider.
* fix(pricing): recompute the audit tier per call and re-derive warm daily caches
Both review blockers:
- audit-report no longer feeds a bucket's SUMMED tokens into the
per-call 272k threshold (a \,979 phantom gap on the reviewer's
corpus, any tiered codex model clearing the threshold on the sum
alone). The recompute accumulates per call, through the same tier
swap calculateCost applies, with each call's own prompt total; a new
fixture mixes one 300k call with 200 small ones whose sum blows past
the threshold and pins that only the big call tiers. Web search
stays on the base rates - it never participates in a tier.
- DAILY_CACHE_VERSION 33 -> 34 with MIN_SUPPORTED_VERSION raised to
match, the cfdaa685 convention: a 33 file written under tierless
pricing is below the floor, is not adopted as the finalized base,
and its days re-derive off the warm session cache (\ of codex
spend on the reviewer's corpus lives only in the daily cache). The
migration tests pin the new floor contract: no version-diff repairs
merge into a below-floor file, and the v30 adoption test tracks
CURRENT.
Nits from review: the kimi precision tightening is reverted to (8, 5),
and the tieredCostsFor comment is rewrapped.
Measured on this machine's corpus, tier-only (branch vs the refresh
branch, same data): +\.43 lifetime codex - this corpus runs far
more sessions past 272k than the reviewer's 8-of-51,801 (+\.012964);
both numbers are real, the corpora differ.
* fix(pricing): preserve base alias selection when bundling context tiers
* fix(pricing): address the 28 Sep review - floor stays at 28, tier bills the implicit cache-write rate
Restore main's two carry-forward tests: with MIN_SUPPORTED_VERSION kept
at 28 (per the review's correction, warm caches re-derive identically
under 41/28, so raising the floor is unnecessary) a v33 file sits in
the adopt-with-repairs window again and main's expectations hold; the
tier's warm-cache coverage stays in daily-cache-version-rederivation.
The tier-omits-slot test bills an implicit cache-write at the input
rate of the costs object in effect (#1544), so the tiered expectation
is 1_000 * cacheWriteCostPerToken('gpt-5.5', tieredCosts).
Note on the Codex estimate path: an estimated prompt can cross a
long-context threshold and tier itself; measured corpora hold no
estimated calls that cross.
* fix(pricing): pin claude-opus-4 primary and keep carried fallback rows hygienic in the refreshed snapshot
The requested regeneration surfaced two upstream moves beside the tier
work. LiteLLM dropped `claude-opus-4`; a refresh carries dropped ids
into the fallback tier, but the Cursor-style alias `claude-4-opus`
resolves against primary rows, so the bare id fell to the snowflake
gateway row and under-priced by 3x. Pinned at Anthropic's list price
in MANUAL_ENTRIES so it stays primary. LiteLLM also repriced
grok-4.6's base input 2e-6 -> 1.25e-6 (tier rates unchanged); the
below-threshold literal follows. Both changes reproduce identically
with main's unchanged bundler on the same live responses.
Today's drops also exposed a gap in the #1483 carry-forward: primary
files legitimately hold @pin, date-suffixed and free-on-both rows, and
carrying those verbatim imports exactly what
tests/pricing-fallback-data.test.ts keeps out of the fallback file
(main's own next refresh fails those tests today). The carry now skips
@pin/date-suffixed keys (no query can arrive in those forms) and
free-on-both rows (an unpriced model falls to expected-free handling);
vendor-prefixed keys stay carriable since queries match them verbatim.
---------
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
* feat(dock): one Claude ring per config directory, with its own sessions and today (#1523)
Rate limits are per account, so a machine with a work and a personal
~/.claude cannot be served by one Claude ring. When the CLI knows two or
more Claude config directories:
- `codeburn quota --format json` adds a top-level `claudeProfiles` array,
one entry per directory (`providers[]` unchanged). Each entry carries
the directory's own windows, read from its own `<dir>/.credentials.json`
(the macOS Keychain fallback stays reserved for the default ~/.claude),
plus that directory's own today totals from a single Claude-only,
cache-backed parse of today's range, filtered per source id exactly like
the menubar's per-config payloads.
- Live sessions carry `claudeConfigSourceId`, so a per-profile ring claims
only its own sessions. Claude Desktop sessions keep their own bucket id
and belong to no profile.
- The Windows Capacity Dock gains a `claudeProfiles` preference (combined,
default; separate draws one captioned ring per directory). Profile rings
keep addressing the `claude` provider id for the provider set, glyphs,
colors and the connect button; the settings row appears only when the
CLI reports more than one profile.
- Captioned rows are taller by two new metrics (caption line + gap),
mirrored in TS and Rust: the page tells Rust the rail is captioned in
the layout request, and the frame carries the effective row extent the
hit-test uses. Non-captioned rails are pixel-identical to before.
- The desktop app's shared dock-prefs schema parses and sanitizes the new
key, so its pane cannot strip it from windows-dock.json.
Closes#1523
* review: opt-in profile reads, today timeout, missing-dir filter (#1570)
- `quota --claude-profiles` gates the per-directory reads: without it the
payload lists profiles ({id,label,path}) only, so a combined rail and
WSL users pay nothing; the Windows dock passes the flag only while its
claudeProfiles preference is separate, and an unknown option from an
older CLI reads as CliOutdated.
- The default ~/.claude profile reuses the claude provider row's result —
no second request, no second macOS Keychain read.
- claudeTodayBySource races the same timeoutMs and answers an empty map
on loss, so a silent child cannot cost the dock every provider's quota.
- Missing config directories are filtered before the >1 check.
- `captioned` applies only while a Claude ring is in the selected set;
profile rows tolerate a list-mode answer; comments trimmed; merge with
#1560 keeps both keepExpanded and claudeProfiles.
* test(quota): set USERPROFILE for the default-dir check; drop issue refs
---------
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
Consumption-share split in `codeburn models`, reworked per review:
- First-party routes only: the split applies to `dsh` (DeepSeek API) and
`zcode` (Z.ai plan) calls. The same model behind a gateway never had
the vendor discount, and reported-cost providers may already carry it;
the gate set stays disjoint from REPORTED_COST_PROVIDERS.
- The numbers agree by construction: both legs are shares of the stored
list-rate cost (peakUSD + offPeakUSD == costUSD, pinned in tests).
The 0.5x off-peak discount stays on the vendors' bills; nothing is
repriced here. peakCostMultiplier and OFF_PEAK_MULTIPLIER are gone.
- Calendars: unverified 2027 projections dropped; the Z.ai
2026-09-25..10-07 all-day off-peak promotion is honored as a temporary
range; deepSeekHolidayCoverageYears() plus a test fail when the
current year falls out of the holiday list.
- Output shape: JSON carries the five peak keys only on rows with a
split, so a report without DeepSeek/Z.ai first-party usage keeps its
exact historical shape; the Z.ai quota note is removed.
- The `codeburn peak` live-status command is left out (scheduling tool,
not usage tracking); it survives on a separate branch for its own PR.
* fix(models): name dot-form Claude minors and future GPT ids consistently (#1530)
One model id resolved to different display names per provider, and new
models showed truncated names until a hand-maintained entry landed:
- deriveClaudeShortName now accepts the dot-form minor some doors write
on disk (GitHub Copilot's session store: claude-opus-4.8), so the name
keeps its minor instead of truncating to "Opus 4".
- Unknown dotted-version GPT ids derive their display name (gpt-5.7-terra
-> "GPT-5.7 Terra") the way unreleased Claude versions already do, so a
new release needs no SHORT_NAMES entry. Legacy bare-major /
date-packaged ids (gpt-9, gpt-4-1106-preview) stay raw, and curated
labels keep winning.
- Pi/OMP/Copilot local display tables now match keys on segment
boundaries via modelKeyMatches, so a bare `gpt-5` key can no longer
capture `gpt-5.5` / `gpt-5.6-luna`; unmatched ids fall through to the
global resolver, making one id show one name on every provider.
- The JetBrains token scan rejects a trailing dot, so `gpt-5` /
`claude-opus-4` cannot match inside `gpt-5.6-luna` / `claude-opus-4.8`.
Fixes#1530
* fix(copilot): list the current JetBrains model ids in the token scan (#1530)
Review of #1531: the tighter #1530 boundary is right, but with no token
entries the newer ids fell through to copilot-anthropic-auto and priced
as Sonnet 4.5. Add the current dotted ids, longest first per the list's
order, so "model":"gpt-5.5", gpt-5.6-luna and claude-opus-4.8 resolve to
their own priced, correctly named rows; unknown future versions (a dot
followed by a digit) still return no token.
* menubar: dock preference for the hover bubble's surface
* menubar: let the dock's hover bubble keep its own surface
* menubar: setting for the dock's hover bubble surface
* docs: dock hover bubble surface setting
OMP logs decision and warming calls outside the transcript as model_usage entries. The parser read only assistant messages, so that spend never reached a report. Each side call now counts at its reported cost, with its purpose in the tools column. Bumps the omp parse version and the daily cache to v41.
* fix(menubar): pick the CLI package manager from the resolved symlink
Homebrew's node sets the npm global prefix to the Homebrew prefix, so a
global npm install puts the codeburn launcher at <prefix>/bin/codeburn
pointing at <prefix>/lib/node_modules/codeburn. The directory substring
test saw /homebrew/ there and chose brew upgrade, which exits non-zero
with "codeburn not installed". Resolve the launcher first and branch on
the target: /Cellar/codeburn/ is brew, /node_modules/codeburn/ is npm.
The manual hint used the same substring test, so route it through the
same decision.
refs #1557
* test(menubar): pin symlink resolution in brew path tests
---------
Co-authored-by: Yi-111-a <>
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
* feat(cursor): import Cursor's usage export to replace local estimates (#1545)
* Price Cursor-hosted grok-4.6-high like grok-4.6 in the Cursor import
* Skip no-op Cursor re-imports and price cursor-grok-4.6-high ids locally
* test(cursor-import): write agent fixtures under os.homedir() so Windows finds them
* fix(cursor-agent): read CLI sessions that exist only in ~/.cursor/chats store.db (#986)
A session with no exported transcript is read from its store.db; one with
a transcript is read from the transcript alone, so nothing counts twice.
Store turns follow the transcript accounting, take their time from the
prompt's <timestamp> tag and their model from the reasoning block, and
merge every conversation root so summarized history is kept. Daily cache
moves to v39 so settled days re-derive.
* cursor-agent: price store.db turns at the Cursor (auto) rate like transcripts
* fix(cache-lock): coalesce heartbeat ticks so the fence never waits behind a backlog
Each interval tick queued a full heartbeat in the owner serializer, so an
interval firing faster than a tick completes grew the queue without bound and
verifyStillOwner waited behind all of it. The 1ms-heartbeat fence test hit its
30s timeout on a loaded runner and retried up to 10 times, which is the silent
cache-lock suite hang in CI.
* test(menubar): hold off the helper's SIGTERM until the stand-in app records its status
The relaunch helper signals the app as soon as the CLI child is reaped, which
could land between the stand-in's fopen and fclose and leave an empty status
file. Also wait for the recorder's full line instead of a fixed sleep.
* test: pin day-boundary tests so they pass at any time of day
CLI children that query "today" now run in a fixed-offset zone where it is
about noon on the UTC date (tests/fixtures/noon-tz.ts), so fixtures a little in
the past are always today; the old clamp to UTC midnight put them in the future
for the first minutes of the day and zeroed the counts. The month-scoped budget
and project-filter fixtures get the same treatment (the latter also failed on
days 1-5 of every month), the cursor lookback case derives its start from now
instead of a date that ages out on 1 Oct, and the renderer tests that format a
stamp from a few hours back pin the clock to mid-day. The session drawer date
is formatted in the local zone, so its expectation is too.
* test: derive dated fixtures from the clock so rolling windows never age them out
The six-month --period all window and the sync ledger's six-month prune
dropped fixtures fixed at April-July 2026 from 1 Nov 2026 on. CLI fixtures now
sit 30 days back (utcDaysAgo), the ledger and push tests pin Date to July 2026,
the --to-only range uses yesterday, and the date-picker tests pin mid-month so
two days back is on the calendar they open (they failed on the 1st and 2nd of
every month). The noon-tz helper moves to tests/fixtures/clock.ts.
* test(desktop): give spawned stand-ins room to boot before the silence window
The watchdog and graceful-kill tests used 1-1.5s silence windows, and the
window before a child's first byte includes node's boot. On a loaded machine
the child was signalled before it installed its handler or wrote its byte.
The windows are now 3s and the last-byte test measures from the byte the child
stamps. The killAll and fallback-scheduler tests wait on their condition with
a 15s deadline instead of a 2-3s one.
Copilot Chat OTel spans carry no github.copilot.git.repository, so every
agent-traces.db conversation fell back to copilot-chat. Match the
conversation id (or copilot_chat.chat_session_id / parent_chat_session_id)
to the workspaceStorage/<hash>/chatSessions file of the same id and name it
from workspace.json, including multi-root and remote folders. Totals are
unchanged; copilot re-parses once and the daily cache moves to v38 so
settled days re-derive the project split.
usage-summary for an Enterprise seat has no individualUsage.plan, a null
individualUsage.overall.limit and teamUsage.onDemand instead of pooled, so
decodeCursorUsage returned null and the adapter reported an unrecognized
response. Fall back to the team on-demand meter, list it beside the
monthly window when both exist, and decode meter amounts as Double in the
menu bar so a fractional amount cannot fail the whole decode.
Settings no longer appends sign-in guidance to transient failures.
Reported in #1546.
* feat(copilot): legacy JSON chat session parser and auto-mode resolution
* fixed copilot naming
* revert: remove unrelated jetbrains and env-isolation changes
* revert priority change
* fix(copilot): route legacy JSON chat sessions cleanly and fix calculateCost web search param
* fix(copilot): legacy chat session parsing and pricing
* fix(copilot): strip github.copilot-chat/ namespace from legacy model ids
found github.copilot-chat prefix in my chat sessions
* fix(copilot): revert comments
* fix(copilot): model names
* fix(copilot): model display name edge cases
* feat(copilot): only keep legacy parser source
* fix(copilot): legacy sessions skip binary tool outputs and use recorded thinking tokens
A screenshot or other binary tool result was base64-decoded as UTF-8 and
char-counted as input, so one 200 KB PNG billed 50k input tokens. Only
text/* payloads count now.
A round's recorded thinking.tokens was compared with the char estimate of
the same text via Math.max, so the estimate won whenever it was larger.
Each round now uses its recorded count when present and the estimate only
otherwise, summed across rounds.
Docs list the flat .json files and the transcript gate covering them.
* fix(models): price Copilot's dotted spellings of Claude 3.5, 3.7 and Opus 4.1
Copilot writes claude-3.5-sonnet, claude-3.7-sonnet,
claude-3.7-sonnet-thought and claude-opus-4.1, which resolved to no
pricing row and billed $0. They alias to the dashed rows now.
---------
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
* fix(opencode): price Vertex turns on a Vertex row and keep an unpriceable recorded cost through the cache (#1547)
OpenCode records the cost of each turn. For an id CodeBurn cannot price the
parser fell back to that cost, but the session cache stored no cost and
re-priced the cached tokens on every read, so the call showed $0. The
recorded cost now rides on the cached call as fallbackCostUSD and applies
only while the tokens price at $0, so a later catalog row or price
override still wins.
The session-level rollup used provider/model while per-message turns use
the bare id since #1543; it now uses the bare id too. google-vertex and
google-vertex-anthropic map to a metered Vertex route.
OpenCode and Kilo Code re-parse once; daily cache v37 re-derives settled days.
* fix(opencode): keep a declared-free id at $0 instead of its recorded cost
A model the user marked flat-rate, priced at a zero rate, or that looks
local prices at $0 on purpose. The recorded-cost fallback now skips those
ids at parse time and on cache read, through one guard in models.ts.
* fix: count every Cursor Agent assistant message instead of one turn per user message
Agentic loops emit dozens of assistant messages per user message and
the parser kept only the first (on one real corpus: 247 of 4,946
messages, ~4% of assistant text). Each message now carries the last
user message forward, tool_use inputs join the output text, and input
tokens use the full user text while the display stays truncated at
500 chars. Parse version bumped so cached sessions re-parse.
* cursor-agent: a transcript that ended before any assistant message is not an unrecognized format
* cursor-agent: bill the user text once per user message, not once per assistant message
* cursor-agent: re-derive finalized days and cached sessions under the new accounting
---------
Co-authored-by: Resham Joshi <65915470+iamtoruk@users.noreply.github.com>
* Store the session cache in day pieces with a per-provider index
Layout v10: a provider's files live in pieces by the UTC day of their
newest call, each with a key file, and an index row per file (piece,
fingerprint, call span, v9 month) read on every load. A ranged load reads
the index, the key files and only the members it reports on; a save
rewrites only the pieces it touched. Rows keep v9's full-load order and
month scope, so orphans, the first-paint snapshot and dedup see what they
saw on v9. The v9 month shards re-lay out once, streaming, and are removed
after v10 publishes.
* Skip cached transcripts outside a narrow query's months when unchanged
v9 could not see them, so every narrow query re-parsed each older Claude
transcript it discovered and republished the cache. The index knows their
fingerprint; an unchanged one needs nothing, a changed one still re-parses
as if uncached.
* Read piece members in batches through one buffer, and full loads concurrently
* Write piece key files without a sync; rebuild lost keys from the piece
* Changelog: day pieces
* Seed dedup with the keys of unchanged transcripts outside the query's months before a whole-file parse
* Upgrade-path check: verify the v10 envelope, indexes, pieces and key files
* fix(menubar): always emit avgCostPerSession for older menubar apps
Menubar 0.9.14-0.9.24 decode topProjects[].avgCostPerSession as required,
so omitting it for partial session counts broke Today with any 0.9.25 CLI.
Emit the 0.9.24 value again and keep sessionCountBasis so 0.9.25+ hides
a partial average. Add a test that checks the payload against the
required keys of every released menubar app.
* fix(menubar): restart the app after an in-app update
The app runs `codeburn menubar --force` as its child, and pgrep/pkill
skip their own ancestors, so the running app was never stopped and
`open` re-activated the old copy. Find the app with pgrep -a; when it is
an ancestor, swap the bundle while it runs, exit, and let a detached
helper stop it and open the new bundle. The terminal path stops the app
first as before.
Reported in #1541, thanks @AleksandrLiadov.
* Write session shards one member per line
A shard is still one JSON object, so older builds read it unchanged, but it
is written through a stream and never exists as one string. Past V8's string
limit the save used to throw and the cache silently never persisted; reads of
such a shard now go member by member.
* Hold cached files a ranged query cannot report on as stubs
A ranged load streams its in-scope shards and keeps in full only the files
with a call in the range, spawn anchors, failure markers, turn-less files and
WSL paths. Every other file becomes a stub: its fingerprint, bucket, PR flag,
dedup keys per turn and where its member line lives. A stub stands in for its
entry everywhere but the summaries it cannot contribute to: reconcile, the
seenMsgIds and parser dedup pre-seeds, the query-time key replay in serve
order, orphan surfacing, eviction and the emptiness checks. A stub whose file
changed loads its member first, so the append or re-parse runs as before, and
a wider request in the same process loads the stubs it now needs.
A bucket holding stubs is rewritten as a full load would write it, copying
the stubs' lines from the shard they came from; unchanged, the published
shard is kept. A single-line shard from an older build is rewritten on the
next save. Providers whose parse reads cached entries no report shows
(hermes, quickdesk, antigravity, devin, gemini) still load in full.
* Discover codex projects from a small index
Discovery only needs each rollout's project, but it loaded the whole codex
results file (hundreds of MB on a large corpus) on every run to get it.
flushCodexCache now also writes codex-projects.v15.json, keyed by the same
fingerprint, and discovery reads that. A missing index is built once from the
results file.
* Load the session cache once for status today and month
status asks for today, then the month. A command can now declare the union of
its ranges, so the first load already holds the month in full and the second
request finds it there; each request keeps its own month scope and summaries.
* Release the pre-lock cache view before reloading
Once the refresh lock is acquired or another process completed the refresh,
the snapshot loaded before the wait is never used again, but it stayed
referenced while the reload read a second view.
* Test the first-paint snapshot serves stubs in load order
* Parse a shard whole when the range covers all its months
A range that spans a shard's months needs nearly every member, and streaming
it one member at a time only added allocation churn (about 200MB more RSS on
report -p all over a 1GB cache). The streaming reader also reuses one read
buffer instead of allocating a fresh one per chunk.
* Parse a shard whole once the range reaches its oldest month
The previous rule also required the range to reach past the shard's newest
month, which a range ending now never does, so report -p all still streamed
every shard.
* Carry a shard line that spans reads as decoded text
Members of a megabyte and more span the 1MB reads, and joining their pieces
as Buffers allocated off-heap memory that only a GC returns: report -p all
over a 1GB cache peaked about 170MB above main in RSS. Decoding the pieces
with a StringDecoder keeps them on the heap and brings every streamed range
below main, so the whole-file parse for wide ranges goes again.
* Changelog: bounded session-cache load
* fix: four cost accuracy bugs in export and pricing
- export daily rows bucket each call on its own day (same rule as the day
aggregator) and no longer skip a turn without a timestamp
- cache-write tokens on a non-Anthropic model with no published write rate
bill at the input rate instead of a fabricated 1.25x (calculateCost and
audit); daily cache v35 and pricing schema 4 so stored figures re-derive
- OpenCode/Kilo messages with output but no usage, and Copilot transcript
calls with no token count, are marked estimated
- export records keep unrounded cost and gain estimated/unpriced columns
* fix: plain-decimal CSV costs, re-parse cached copilot transcripts for the estimated flag
* ci(upgrade-path): compare copilot records at cent precision across the export rounding change
* fix(opencode): keep pre-upgrade history on OpenCode 2 databases and price v2 turns like 1.x
OpenCode 2 copies 1.x sessions into session_v2/session_message on first
start, but the copy drops compaction summary and task-tool turns, skips
messages it cannot decode, and can stop partway. A session present in both
generations now reads its frozen legacy turns first and adds only the v2
turns legacy does not hold (migrated rows keep their ids, so dedup skips
the copies).
v2 turns also used a provider/model id that failed pricing for ids such as
opencode/claude-haiku-4-5; they now use the bare id 1.x stored.
Reported by a user.
* Changelog: OpenCode 2 unpriced ids showed as $0