Upstream `main` ships a provider this branch has no counterpart for: the
Cline CLI (npm `cline`, 3.x), whose sessions live in a layout unrelated to the
VS Code extension tree `cline.ts` reads. Files added on one side only produce
no merge conflict, so a `main` merge would happily create `src/providers/
cline-cli.ts` — a path npm workspaces does not build and no registry imports.
Green build, and a whole provider quietly does not exist.
Ported from #940 (@ozymandiashh), which carried it faithfully: identical dedup
keys, all 34 upstream test cases, and the clean phase-8 split — discovery and
file I/O host-side, pure record decode in @codeburn/core, registered next to
its neighbours and deliberately separate from the shared vscode-cline tier.
Two changes on top of that port:
- `observations.ts` follows the post-#1074 conventions: the model is routed
through `normalizeModelIdentifier` at the observation boundary like every
other provider, and measured cost is carried the way the sibling decoders
carry it rather than through a cast.
- the estimated-cost path reports zero web-search requests. Upstream prices
that path with a hardcoded 0; letting the decoded `fetch_web_content`
count reach the pricing pass would bill $0.01 per fetch on top of tokens,
a billing change nobody asked for. A metered call keeps the real count —
its dollar figure comes from the CLI, so nothing prices off it.
DAILY_CACHE_VERSION takes 27 (MIN_SUPPORTED 27): every historical Cline CLI
session contributes usage no older rollup ever contained, and usage-aggregator
serves every day before today from that cache for ten years, so without the
bump an upgrading user would keep cline-cli-less history forever while today's
numbers silently included it.
Resolves daily-cache version coordination: base landed v25 (#930); this
PR's kiro chat-file fix takes the next number, v26, since it changes
historical kiro chat-file numbers and needs its own one-time
re-derivation. Session-cache kiro parse fingerprint bump (v1 -> v2) is
kept alongside base's opencode fingerprint addition. CHANGELOG entries
combined (keep-both), with the kiro entry's cache-version reference
updated to v26.
Ports three upstream commits this branch never received: the attribution
feature (1bf7206), the review hardening on top of it (ccee28a), and the
security follow-up that closed credential-leak paths and added session
retraction (50c8251).
They are ported as an end state rather than in sequence. Two and three revise
one, so replaying them in order would have introduced the very issues they fix
and then removed them again — and anything missed in the third pass would have
shipped a feature with a reopened hole, which is the specific way this port
could have gone wrong. The credential-leak paths that commit closes are
enumerated and checked off individually against the result.
One correction to an earlier draft of this message, which claimed no new
unkeyed digest is introduced. That was wrong: stateHash in sync/otlp.ts is a
new unkeyed sha256, and it feeds deriveSpanId, so it is an input to a value
that goes on the wire. It is not a D1 violation — D1 governs core's fingerprint
module and its caller-supplied key, while stateHash is a local ledger
discriminator computed over data that is itself sent in cleartext, so it hides
nothing and leaks nothing. But the sentence was false and is worth correcting
rather than quietly dropping.
This branch now carries #931's commit (c467548, "fix(sync): key the device,
span and trace digests") beneath this one — cherry-picked onto the shared base
so the history stays two clean commits. That ordering is load-bearing:
reconciliation is mandatory in every merge order, not optional. git merge-tree
reports no conflict against #931 in either direction, yet the merged file does
not compile: #931 drops the createHash import and gives the derive functions a
privacyKey first argument, so an unreconciled attribution section leaves
stateHash with an undefined symbol and two one-argument call sites. Rebasing
replays the same breakage, which is why the earlier "land #931 first, OR
reconcile" framing was wrong.
The two call sites are reconciled INTO #931's keyed signatures, in the
direction #931 demands: buildAttributionOtlpPayload obtains the persisted host
privacy key exactly as buildOtlpPayload does — one getPersistedHostPrivacyKey
call per builder, no second source of the key — and threads it into
deriveTraceId and deriveSpanId. This is the security point of the
reconciliation: loosening the signatures back to one argument would
reintroduce exactly the unkeyed span and trace ids #931 exists to remove, in
new code.
stateHash stays unkeyed, deliberately: it is a local ledger discriminator over
One more merge-compat fix, in #931's own test file (sync-privacy-key.test.ts):
the concurrency fixture path was built from process.cwd(), which is the repo
root under `--root packages/cli` — the worker then exited on a nonexistent
file before writing its ready file and the race test timed out. The path is
now anchored to the test file's own location (fileURLToPath(import.meta.url)).
This is the only line of #931's tree this branch touches; 37a5b46 remains a
verbatim copy of c467548.
Second fix in #931's tree, same motivation: the concurrency race test
adopted with only a 50ms budget. createKeyFileExclusive polled the winner's
file 5x10ms after EEXIST, and the strict entry check refused an 'invalid'
file INSTANTLY — but the winner's create (open) and write are separate
syscalls, and under load the loser can read the still-empty file either at
entry or inside the poll. Both windows now share one bounded awaitValidKey
(500ms) that ADOPTS the winner's key when it lands and otherwise throws the
same refusal. Nothing is ever overwritten; a file left invalid by a crash or
truncated write still fails loudly. This is the second #931 file this branch
touches; 37a5b46 remains a verbatim copy of c467548.
The sync path derived three identifiers with bare SHA-256 and sent them to a
configured endpoint.
`deriveDeviceId` hashed `hostname:username` and truncated to 64 bits, commented
"pseudonymous, stable". An unkeyed digest of a host and username pair is not
pseudonymous against anyone who can guess plausible values: hash the guess,
compare, done. `deriveSpanId` hashed the dedup key — and for pi, zerostack,
lingtai-tui and codebuff that key embeds the raw absolute source path, home
directory included, because the bridge passes `source.path` straight through.
Guess a plausible home and project name and the same confirmation works.
This is the project's own standard, not an outside opinion. Decision D1 requires
a caller-supplied HMAC key for fingerprints precisely so digests of paths cannot
be dictionary-attacked, and core's fingerprint module throws on an empty key to
enforce it. The sync path bypassed the primitive entirely. It also contradicted
the project's own user-facing guarantee: docs/sync/README.md promises that
code, file contents, diffs and PATHS stay local, and the unkeyed span id shipped
absolute paths (for the four providers above) in a form confirmable by anyone
with a plausible guess.
All three ids are now HMAC-SHA256 under the per-install privacy key — the same
key core's fingerprints use — with domain prefixes so one value in two
positions never yields the same digest, and composite inputs joined with the
same ASCII Unit Separator (0x1f) core/fingerprint.ts uses so a value containing
':' cannot forge a field boundary. The derive functions throw on an empty key
rather than degrading. The payload builder obtains the key itself, so the
decode path, which runs with an empty key by design, never reaches it.
Sync now REQUIRES the persisted key: privacy-key.ts exposes a strict variant
that aborts the push instead of falling back to per-process randomness when the
config dir is unwritable, and refuses to silently regenerate a key file that
fails validation (truncated by a full disk, a partial write). Cross-process id
stability is load-bearing — partially rejected batches are not ledgered
precisely because deterministic span ids make full-batch retry safe — so a
per-process fallback key would emit fresh ids on every retry and let the backend
double-count accepted spans, and a silent re-key would orphan everything already
pushed. The fingerprint consumers keep the tolerant fallback: they only need
per-process stability.
The refusal is now complete, and enforced for every corrupt shape: "no file at
all" is the only state a first use may create. A file that exists but is
unreadable, zero-byte or whitespace-only (a partial write), or fails hex
validation aborts the push and is left untouched — treating those as MISSING
would silently regenerate the file and re-key every id, which is exactly the
case the strict path exists to refuse. First creation is also exclusive
(O_CREAT|O_EXCL): when two processes race the first use, the loser re-reads and
adopts the winner's key, so concurrent pushes can never mint different keys and
mix cached device ids with spans derived from the other.
Scope, stated honestly: sync is opt-in and needs an endpoint plus credentials,
the digests are of identifiers rather than prompts or file contents, and this
predates the extraction. It is not an active leak of user content. It is a weak
construction the project already knows how to do properly. This change narrows
the exposure rather than closing it: ai.project still ships a project name in
the clear, and in one Claude fallback path that name is a dash-encoded absolute
path.
Blast radius: every id is re-keyed once at upgrade, so anything already pushed
stops correlating with new sends and the backend sees a fresh device identity.
Ids stay stable afterwards unless the key file is lost. The host-side sent
ledger keys off the raw dedup key and is unaffected, so re-push filtering keeps
working.
decodeKiroChatFile estimated input tokens from pendingUserMessage - the last
human turn sliced to 500 chars - while output summed every bot char, so any
multi-turn session or final prompt over 500 chars under-reported input tokens
(and therefore costUSD) severalfold.
The estimate is now the sum of every human turn's full character count, with
the 500-char cap kept for the display userMessage only. That closes most of
the gap but does NOT reach parity with the modern-execution, CLI-session and
V2 arms: those count tool and system records as input - their code comments
state tool results are fed back to the model - while the chat arm still
counts only human records. Tool content demonstrably exists in the format
(the G2 fixture carries a tool record), so the chat arm still under-reports;
it just under-reports far less than before. The blast radius is the
chat-file arm alone: the IDE-file dispatcher routes any record carrying a
chat array plus metadata to decodeKiroChatFile, so this is chat-shaped Kiro
IDE files, not every Kiro prompt.
The identity-preamble exclusion now trims leading whitespace before its
startsWith match. Pre-fix, a near miss (a leading newline, a BOM, a wrapper)
was nearly harmless, because the preamble only mattered if it happened to be
the last human turn; post-fix, every unmatched system-injected human record
adds its FULL length to input, and preambles are large - a missed match is a
silent multi-thousand-token inflation on every affected session. Leading
whitespace tolerance is cheap (a genuine prompt never starts with whitespace
plus an identity tag) and the failure asymmetry favours exclusion: a false
negative inflates tokens, a false positive only skips a preamble. A renamed
preamble remains a residual risk, noted in the near-miss regression test.
Cached history is affected, which is what a user actually sees. session-cache
serves unchanged files without invoking the provider parser, so bump kiro's
PROVIDER_PARSE_VERSIONS fingerprint (ide-parsing-v1 -> v2) to force one
re-parse of every already-cached kiro session; without it the pre-fix token
and cost numbers would be served forever.
The daily rollup ALSO needs invalidating for this fix to be fully visible:
days finalized before the fix keep their pre-fix kiro cost in the daily
cache, and ensureCacheHydrated re-derives them only on a version bump, a
savings-config change, a timezone change, or an incomplete cache — the
session-cache re-parse alone leaves finalized day totals untouched. So this
commit bumps BOTH layers: the session-cache PROVIDER_PARSE_VERSIONS
fingerprint above forces the one re-parse of every already-cached kiro
session, and DAILY_CACHE_VERSION (15 -> 17, MIN_SUPPORTED_VERSION raised
with it; 16 is skipped because main already claimed it for the codex
structural-discovery fix, and claiming 16 here would load a main-built v16
cache as current and complete, so the invalidation would never fire) forces
the daily rollup's one-time re-derivation, so finalized day
totals are rebuilt under the corrected estimate. The re-derive reaches every
day whose kiro chat files still exist; sourceless days carry forward with
their pre-fix totals under the v14 NEVER-LOSE rule (a carry-forward, not a
refresh — nothing can reconstruct them once the files are gone).
Update the G2 parity golden: A1 was pinned at 125 tokens for a 3000-char
prompt (the 500-char slice / 4); the corrected value is 750 (3000 / 4), with
a comment marking 125 as a pre-fix value so it is not restored. 3000, 2400
and 1000+1000 are all exact multiples of four, so add G2b pinning the
estimator's rounding with an odd length (3001 chars -> 751 tokens; round and
floor would both give 750). Add a money-path regression test (2400-char
prompt -> 600 tokens, userMessage still 500-capped for display), a
multi-turn accumulation test (an identical resubmitted prompt counts again -
a real second model input; identity messages stay excluded), a near-miss
identity test (leading-newline and BOM preambles stay excluded), extend the
kiro cache-invalidation test to pin the v1 -> v2 fingerprint bump, and add a
daily-cache regression test seeding a complete pre-fix v15 cache (unchanged
savings hash and timezone, so nothing but the version bump can invalidate
it) and proving the bump forces the re-derive that lands the corrected kiro
cost — while the v15 file is never rewritten.
The desktop sessions resolver returned a single per-platform path, so
Microsoft Store (MSIX) installs of Claude Desktop were invisible: their
data lives under %LOCALAPPDATA%\Packages\<Claude package>\LocalCache\Roaming\Claude\local-agent-mode-sessions
and a filesystem junction workaround breaks Cowork's own file access
(reported and verified in #611).
getDesktopSessionsDir() becomes getDesktopSessionsDirs(): an ordered,
deduped candidate list (override, then classic APPDATA, then MSIX packages
matching Claude_* or *.Claude_*, existence-checked, lexicographically
sorted; .config on Linux). Results are memoized per env-input tuple so the
parser's per-file classification never rescans Packages. All call sites
scan every candidate; macOS, Linux and classic Windows behavior unchanged.
Fixes#611
- Resolve k3/k3-agent/k2d6-agent model aliases to canonical Kimi names
- Discover sessions across all Kimi Code homes (CLI + desktop runtime)
- Accept conv-*/ctitle-* session directory naming, not just session_*
- Add Kimi Code provider tab with brand color to the menubar
- Show short model names (Kimi K3, Kimi K2.6) in the menubar payload
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.
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.
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
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
Dependency-ordered plan for the standalone Electron app. Corrects spec data
gaps: yield/plan already emit JSON; only spend flow-json (Sankey matrix) and
devices/share --format json are new. Subagent-driven execution: Opus 4.8 +
Codex 5.6-high implement, Fable reviews each task.
Standalone Electron app rendering the v6 "indigo instrument" wireframes,
fed by the codeburn CLI via the menubar JSON contract (spawn codeburn --json,
decode, poll). Six sections: Overview, Spend, Optimize, Models, Plans,
Settings/Devices. Data aggregation stays CLI-side; renderer never fakes data.
JetBrains Copilot plugin ≤1.5.x (e.g. 1.5.59-243) stores all session turns
inside ONE large binary-framed outer Nitrite document, rather than the
per-turn {"__first__":{"type":"Subgraph",...}} blobs introduced in later
plugins (≥1.12.x, e.g. 1.12.1-251).
In the old format each assistant turn is a UUID-keyed Value entry whose
value field contains a JSON-string-escaped AgentRound record:
{"<uuid>":{"type":"Value","value":"{\"type\":\"AgentRound\",
\"data\":\"{...reply...}\"}"}, ...}
The extractResponseText depth-unescape loop already handles this one extra
level of escaping; the only gap was that extractJetBrainsDbTurns never fed
it the outer document — it only scanned for __first__/Subgraph blobs, which
the old plugin never writes.
Add a fallback that activates when the Subgraph scan produces zero turns but
'AgentRound' text is present in the raw file (old-format signal). It locates
the binary-framed outer document (UUID-keyed Value entry, hex matched
case-insensitively so an uppercase UUID does not fall through to $0), extracts
it with matchJsonObject, and passes it to extractResponseText. Because the outer
document holds every turn in one blob, this emits ONE session-level call per
document (all rounds' replies joined): cost/tokens are correct, only the
per-turn call-count granularity is coarser — an accepted tradeoff for legacy
data. MVStore keeps two identical collection copies; seenReplies dedupes them.
The fallback is guarded by turns.length === 0 so new-format sessions (whose
Subgraph scan succeeds) are completely unaffected and never double-counted.
Tests: old-format doc with multiple AgentRound rounds → 1 call whose token
count equals the two non-empty replies joined (the empty tool-call round is
excluded); an uppercase-UUID variant (fails without the case-insensitive
match); and a guard that new-format Subgraph turns are not double-counted.
docs/providers/copilot.md documents the old format and the one-call-per-session
limitation.
JetBrains Copilot has two turn shapes in the Nitrite .db:
- ask mode — the reply is a `Markdown` record's `text`;
- agent / plan mode (e.g. PyCharm agent sessions, `/plan …`) — the reply is the
`reply` field of an `AgentRound` record, and the `Markdown` record instead
holds the USER's prompt.
extractResponseText only read Markdown, so agent-mode turns yielded no reply
text: they were discovered (session/turn counts showed up) but priced at $0
because output tokens came out zero. On this machine that silently
under-counted a PyCharm session ($0 → $0.35) and several IntelliJ agent turns.
Determine the mode by the PRESENCE of an `AgentRound` record and read only that
record's `reply` (collecting every non-empty round in a multi-round blob).
Crucially, an agent blob whose reply is empty — a failed turn or a pure
tool-call round — does NOT fall back to the Markdown record, so a user prompt
is never mistaken for the assistant's output; such turns bill $0 as before.
Ask-mode blobs (no AgentRound) keep reading Markdown. Plan mode's sidecar
records — Thinking, PendingChanges (proposed diff, under `content`), AskQuestion,
Notification, SubTurn, and file-read `text` results — are never read as output.
Verified across all local stores: the two reply shapes never coexist in one
blob, so the split is unambiguous.
Tests: agent-mode reply extraction (ignoring the prompt Markdown), pure
tool-call rounds → $0, multi-round collection, and a failed agent turn → $0.
docs/providers/copilot.md documents both turn shapes and the ignored sidecar
records.
## What & why
The JetBrains Copilot plugin (IntelliJ, PyCharm, RubyMine, …) stores its
chat/agent sessions under `~/.config/github-copilot/<ide>/<kind>/<storeId>/` —
a location none of the existing Copilot sources (CLI JSONL, VS Code chat
sessions/transcripts, OTel SQLite) read. As a result all JetBrains Copilot
usage was silently uncounted in every CodeBurn report. This adds a reader for
that store so those sessions are discovered, priced, and attributed to the
right project.
## How it works
- **Reader.** The store's session content is a Nitrite `.db` — an H2 MVStore of
Java-serialized documents. It is scanned as `latin1` for byte-offset
stability: no Java deserializer, no new dependency, and it is not SQLite so
`node:sqlite` is not involved.
- **Reply text.** Assistant replies live in nested-escaped
`{"__first__":{"type":"Subgraph"…}}` blobs. The text is recovered by
unescaping one level at a time and, at the depth where the Markdown record's
`data` field is a well-formed one-level-escaped JSON document, reading it
structurally — so a reply containing its own quotes is never truncated or
duplicated (which would otherwise inflate the estimate).
- **Tokens/cost.** The store records no token counts, so output tokens are
estimated from the reply text (`CHARS_PER_TOKEN = 4`, re-decoded
latin1→utf8 so multibyte replies count by codepoint) and every call is marked
`costIsEstimated`. Failed generations (error status, no reply) are billed $0.
- **Sessions.** One `.db` holds many chat tabs; turns are grouped back to their
conversation GUID so the UI shows one session per tab, deduped by reply
content per conversation.
- **Project attribution**, most authoritative first:
1. the plugin-recorded `projectName` field (JetBrains Copilot 1.12+), joined
across kind dirs by store id — the billable turns live in
`chat-agent-sessions`, but the label is usually written into the sibling
`chat-sessions`/`chat-edit-sessions` store. Read length-delimited and
re-decoded latin1→utf8 so non-ASCII repo names round-trip.
2. the `.git` repo root of a referenced `file://` path.
3. a generic `copilot-jetbrains` bucket when neither signal exists.
The conversation title is a chat-thread name, not a project, so it is kept
out of the project field and surfaced as the session label instead.
Override the JetBrains github-copilot root with
`CODEBURN_COPILOT_JETBRAINS_DIR`.
## Docs
- `docs/providers/copilot.md` — full JetBrains section (store layout, latin1
scan, reply extraction, projectName precedence + cross-kind join).
- `docs/providers/README.md` — Copilot storage updated to note the Nitrite .db.
## How to verify
- `npm test -- copilot` and `npx tsc --noEmit` (fixtures reproduce the real
nested-escaped .db framing, including quote- and multibyte-bearing replies).
- End to end against a real install:
`CODEBURN_CACHE_DIR=$(mktemp -d) node dist/cli.js status --provider copilot \
--period all --format menubar-json`
— JetBrains sessions appear By-Project under their real repo names.
- Set `CODEBURN_COPILOT_JETBRAINS_DIR` to a fixture root to parse a controlled
store without touching the real config dir.
Bump the version, document the Zed provider (logo, provider doc, data
table row), add the audit and context commands to the README, and
refresh the stale Cursor data-source description to the composer-meter
accounting.
Pi and OMP have no dedicated skill tool: a native skill load is emitted as
an ordinary `read` tool call whose path points at the skill's SKILL.md (or a
`skill://<name>` URI in newer OMP builds). The parser mapped every read to the
Read tool and never populated `skills`, so Pi/OMP sessions over-counted Reads
AND always showed an empty "Skills & Agents" breakdown.
Detect these reads (basename === 'SKILL.md', or a skill:// URI), extract the
skill name (parent directory, or the URI segment), and surface the call as the
`Skill` tool with the name recorded in `skills` -- exactly how the Claude
parser represents a skill invocation. That both removes the Read over-count and
lets the shared classifier tag the turn `general` so the Skills & Agents
breakdown picks it up (populating a field the dashboard never received before).
The path is read from arguments.path with a defensive arguments.file_path
fallback.
Tests cover SKILL.md / skill:// / file_path detection, a non-skill read staying
a Read, and an end-to-end check that a parsed skill load reaches the classifier
subCategory that feeds skillBreakdown.
VS Code GitHub Copilot Chat users with no OTel agent-traces.db and no
~/.copilot/session-state/ saw $0.00 cost: codeburn never read VS Code's
core chat persistence, the only on-disk source carrying their token counts.
Add a fourth Copilot source that reads the VS Code chat delta-journals at
workspaceStorage/<hash>/chatSessions/*.jsonl and
globalStorage/emptyWindowChatSessions/*.jsonl. The files are a kind:0
snapshot / kind:1 path-set / kind:2 array-append journal; we replay it
(prototype-pollution-safe: __proto__/prototype/constructor segments are
rejected and containers use Object.create(null)) and read each request's
result.metadata.promptTokens / outputTokens (falling back to
completionTokens) and resolvedModel for pricing.
Dedup so users with multiple sources are not double-counted: prefer OTel
(skip chatSessions discovery when an OTel source is present), and skip a
workspace's legacy GitHub.copilot-chat/transcripts when that workspace has
chatSessions. New env overrides CODEBURN_COPILOT_GLOBAL_STORAGE_DIR and the
existing CODEBURN_COPILOT_WS_STORAGE_DIR keep discovery testable.
Tests cover the reporter's real request shape (promptTokens 32543 /
outputTokens 60 -> non-zero cost), empty sessions, emptyWindow discovery,
append-then-edit replay, requestId dedup, prototype-pollution paths, the
transcript skip, and the OTel-prefer skip.
* fix: add mssing support for ATIF v1.7
Co-Authored-By: bmcdonough <18721778+bmcdonough@users.noreply.github.com>
* fix(devin): drop unused MCP-coupled types, dead guard, harden metrics fallback
- Remove the @modelcontextprotocol/sdk JsonSchemaType import and the unused
ToolDefinition/FunctionDefinition types it served; type Agent.tool_definitions
as unknown since the parser never reads it (no dependency warranted).
- Remove isImageContentPart: unused, and its `"image" in part` check could never
be true (image parts carry `source`/`type`, not `image`).
- Use the `type` discriminant in isTextContentPart instead of property presence.
- getMetricsFromStep: fall back to legacy metadata.metrics when step.metrics is
present but empty, so a partial metrics object cannot silently zero usage.
- Tests: cover the empty step.metrics fallback and image-only message normalization.
---------
Co-authored-by: bmcdonough <18721778+bmcdonough@users.noreply.github.com>
Co-authored-by: AgentSeal <hello@agentseal.org>
Reads session-level usage from Hermes SQLite state databases
(~/.hermes/state.db and per-profile state.dbs). Tracks input,
output, cache read/write, reasoning tokens, stored costs, tools,
shell commands, and inferred projects.
- Provider reads both root and profile state databases
- Cost fallback: actual_cost_usd > estimated_cost_usd > LiteLLM pricing
- inferProject filters to user/system messages only
- Discovery query capped at LIMIT 10000
- sanitizeProject handles repeated leading separators
- All five review items from PR #386 addressed
- Tested against real Hermes data (557MB state.db with 198 sessions)
Closes#368. Supersedes #386.
Reads ZCode CLI usage from ~/.zcode/cli/db/db.sqlite. The model_usage
table has exact per-request tokens; cost is computed from the pricing
table since ZCode stores none (GLM-5.2 runs on the z.ai start-plan
subscription).
- Split cached tokens out of input_tokens (OpenAI-style) so fresh input
and cache reads price correctly
- Attach each turn's tool calls to one request to avoid double-counting
- Map GLM-5.2 to glm-5p1 (GLM-5.1 rate) until LiteLLM lists it
- Register as a lazy SQLite provider; add test and provider doc
Adds Grok Build (xAI coding CLI) as an eager provider with caching- and compaction-aware token estimation, real tool/shell extraction, Skills & Agents from spawn_subagent, grok-build pricing, and SuperGrok plan presets. Tested against real sessions; full suite green.
* Add zerostack provider scaffold
Register zerostack (gi-dellav/zerostack) as a core provider reading
plain-text sessions from $XDG_DATA_HOME/zerostack/sessions/. Parser is
modeled on pi.ts; on-disk schema is unverified and must be confirmed
against real sessions before a PR (see docs/providers/zerostack.md).
* Rework zerostack provider against real session schema
Replace the initial scaffold (modeled on pi.ts JSONL) with the verified
format from a real local run and the zerostack source:
- Sessions are one JSON file per session under the platform data dir
(~/Library/Application Support/zerostack/sessions on macOS,
$XDG_DATA_HOME/zerostack/sessions on Linux; ZS_DATA_DIR overrides).
- Tokens are cumulative session totals, not per-call, so emit one
ParsedProviderCall per session from total_input/output_tokens.
- Recompute cost via LiteLLM (calculateCost); OpenRouter ids arrive
route-prefixed and resolve through canonical names.
- zerostack persists only final assistant text, so tools/bash are empty.
Verified against a real session (DeepSeek v4 Pro via OpenRouter):
34.1K in / 961 out / $0.016, matching the recorded total_cost. Adds a
fixture-based test and rewrites the provider doc to match.
Maintainer review fixes on top of the OTel cache-token work:
- Remove all DEBUG_OTEL console.warn scaffolding from parser.ts and
copilot.ts (gated but unlike the rest of the codebase).
- Parameterize the spans IN (...) query instead of string-interpolating
trace IDs.
- Fold the per-chat-span metadata query into the trace-span query to drop
the N+1 (one query per chat span -> one per conversation).
- Guard epochToISO against null/NaN/0 so a malformed start_time_ms row no
longer throws on new Date(NaN).toISOString().
- Remove dead code: parseSpanAttributes, OTelSpanRow, and the unreachable
`if (!db) return`; drop unused catch bindings.
- Note in parseProviderSources that the non-durable append path assumes
unique source paths.
- Document the OTel source in docs/providers/copilot.md: Node 22+
requirement, durable-cache monotonic totals, and the one-time
parse-version cache reset on upgrade.
Opt-in Vercel AI Gateway provider (AI_GATEWAY_API_KEY/VERCEL_OIDC_TOKEN), inert without a key. Cursor lookback now period-aligned with a 6-month floor. Gateway fetch hardened on merge with an 8s timeout and try/catch fallback.
* feat(providers): add coder/mux as a datasource
Reads coder/mux session data from ~/.mux/sessions/<workspaceId>/chat.jsonl and normalizes per-assistant-message token usage into ParsedProviderCall. Discovery resolves project names from config.json; the token decomposition matches mux's own inclusive input/output accounting, and cost is recomputed via LiteLLM. Includes unit tests and a provider doc.
Change-Id: Ie6ce9d41254d8bf05ff0965b443328dfa7b598de
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>
* fix(providers): discover mux sub-agent transcripts
discoverSessions only globbed sessions/<id>/chat.jsonl and skipped each
spawned sub-agent's transcript at
sessions/<id>/subagent-transcripts/<childTaskId>/chat.jsonl. Against a real
~/.mux (629 workspaces) those nested files are 5,889 sessions and ~51% of all
assistant messages, so sub-agent spend was silently dropped: total mux usage
went from $17,113 to $21,564 (+26%) once counted.
Walk the subagent-transcripts dir per workspace and attribute each child
session to the parent's project. Dedup is unaffected: the parser keys off the
<childTaskId> dir name, which is disjoint from every workspace id, so each call
is still counted once. Cross-checked parsed per-model token totals against
mux's sibling session-usage.json.
Also corrects the provider doc, which wrongly claimed sub-agents get their own
top-level sessions/<id>/chat.jsonl, and documents the Google reasoning>output
decomposition edge case.
Change-Id: I1d43f26eec7c9c6c523e5ea541e2ff8d0c3aa07e
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds extensionless session index and nested execution file discovery
while preserving legacy .chat support. Modern execution JSON is parsed
for identifiers, timestamps, model IDs, conversation text, structured
tools, and token estimates. Also fixes dedup key poisoning on invalid
timestamps in the legacy parser.
* Capture Antigravity CLI usage via statusline
* Harden statusLine hook: cap stdin at 1MB, fix JSONL file permissions
- readStdin() now rejects input exceeding 1MB to prevent OOM from
a buggy or runaway caller.
- Switch from appendFile (mode only on creation) to open(path, 'a', 0o600)
so the JSONL file is always created with private permissions.
- Set cache directory mode to 0o700.
---------
Co-authored-by: iamtoruk <hello@agentseal.org>