Bug-hunt follow-ups on the realized-savings report:
- Scale the displayed estimate to the measured window so the Estimated and
Realized columns are comparable: mcp/archive use the per-session baseline
times the post-window session count, read-edit uses deficitThen times the
same edits denominator as realized (so realized never exceeds it). The
at-apply estimate stays in --json as estimatedAtApply next to
estimatedForWindow; the footer states the scaling and that mcp/archive
realized figures are derived from session counts, not independently
measured.
- Never crash on a corrupt journal: records without a string status or a
parseable string `at` are skipped and surfaced ("N malformed records
skipped"); the optimize header computation is additionally wrapped so any
error just drops the header.
- Zero post-window sessions now reads "not measurable: no sessions in the
window yet" instead of measured-zero.
- The optimize header sums only normal-confidence measured rows (under-claim);
low-confidence rows stay visible in act report only.
- Tests: floor discipline on non-integer mcp and read-edit products (a ceil
mutation fails), the project-scope keeper subtraction, the archive
estimate==realized tautology, malformed-journal robustness, and the
low-confidence header exclusion.
Capture a trailing-14-day before-baseline when a fix is applied and
re-measure it against the post-apply window so optimize can show realized
numbers next to estimates.
- ActionBaseline (windowDays, capturedAt, estimatedTokens, sessions, metrics)
persisted by runAction; captured in the optimize --apply flow and at guard
install time.
- codeburn act report [--json]: applied, not-undone actions older than 3 days,
re-running the detectors over apply-date-to-now (capped 30 days). Per-kind
realized deltas: MCP/archive tokens-per-session times saved sessions with
reverted-by-user detection; read-edit deficit reduction; guard yield split
labeled correlation. Bash cap is marked not measurable (result sizes are not
retained). Low confidence under 20 post-window sessions or past a 2x volume
shift. Realized numbers rounded down, estimate kept visible.
- optimize gains one header line only when a measured action exists, and
appends "(previously applied <date>, re-flagged)" to re-triggered findings.
No change for users with no applied actions.
Reuses scanAndDetect helpers over a date-bounded range; exports the token
constants and read/edit tool sets rather than duplicating the math.
Claude Code rewrites each assistant message several times as it streams,
every copy carrying the full final usage; the shipped parser dedupes these
last-wins (dedupeStreamingMessageIds) but the guard fold summed every line,
measuring real sessions at 2.5-2.8x their true cost and false-blocking the
hard cap at roughly 40% of the configured spend.
- The session cache now maps message id -> that id's cost contribution and
each id-carrying line replaces its previous contribution; id-less lines
keep plain adds. Validated against two real transcripts (90MB and 116MB):
guard totals now equal the shipped deduped totals exactly.
- Replace semantics also self-heal the trailing-line case: a complete final
line without its newline is folded but byteOffset stops before it, so the
next invocation re-reads it as a replace, not a double add.
- editCount becomes a set-once sawEdit boolean so duplicate copies of an
edit tool_use cannot inflate it; cache schema bumped to v2 (old caches
cold-reparse once).
- Per-session state moves to guard/sessions/ so a session id can never
collide with the shared flags.json, dropping the doAllow special case.
- The git-commit detector now requires commit as the git subcommand at a
command boundary (start of string or line, or ; & |), with intra-command
gaps that never cross newlines: 'git log --grep commit' and
'git diff && echo commit' no longer match, while newline-separated
'git add ...\ngit commit' in multi-line Bash calls now does (verified as
a real false negative on a live transcript).
- Corrected the statusline protocol note: each stdout line renders as its
own row; we emit exactly one.
- New tests: streaming-duplicate fixtures (3x identical, growing last-wins,
incremental replace) asserted equal to a cold shipped-parser computation,
the trailing-partial-line scenario, the commit-detector matrix, and a
stale-plan test proving guard-install plans carry expectedHash (a
concurrent settings edit aborts the apply and survives). The act list CLI
spawn test now anchors to the repo root from the test file location.
codeburn guard install|uninstall|status|refresh|allow plus the internal
hook/statusline handlers. Off by default, fully local, cleanly removable.
- Settings edits go through the action journal (guard-install / guard-uninstall)
with expectedHash, appending our entries and removing exactly ours by command
prefix; a byte-identical uninstall is asserted by test.
- PreToolUse budget cap (soft warn once, hard block with a per-session allow
override), Stop yield checkpoint (expensive with no edits and no commit, once),
and a flagged-project SessionStart opener built from the optimize detectors.
- Incremental per-session cache keyed by session id: resumes the transcript
parse from the last complete-line byte offset via readSessionLines, folding
only the tail into running totals. Warm invocation ~0.28s against a 90MB
transcript, dominated by CLI startup; the tail parse itself is negligible.
- All handlers fail open: any error, malformed stdin, or missing transcript
exits 0 with no output so a broken guard can never block a session.
- Hook protocol verified against the live docs (dated block at the top of
hooks.ts); zero new dependencies.
Plans serialize the full post-edit file content at build time, so a
target edited between the preview and the interactive confirm would be
silently overwritten with stale content (recoverable via backup, but a
silent violation of the dry-run-shows-exactly-what-changes principle).
- PlannedChange edit/create variants gain optional expectedHash: sha256
of the raw on-disk bytes the plan was built from (hashed before the
BOM strip), null when the plan expects the file to be absent,
undefined to skip validation (framework back-compat).
- plans.ts sets it everywhere a target is read: ConfigDocs hashes the
raw buffer it parses, and the marker builders hash the file behind a
shared markerChange helper.
- runAction validates all expected hashes after snapshotting and before
the first mutation; a mismatch throws "<path> changed since the plan
was built; re-run codeburn optimize --apply", removes the backup dir,
and journals nothing. No rollback is involved since nothing mutated.
Tests: stale edit target rejected with no journal record and no backup
dir left, expects-absent plan rejected when the file appeared, matching
hash still applies, and a hash-less change still applies unchecked.
Review fixes plus one coordinator amendment on top of the initial
optimize --apply implementation.
- mcp-project-scope no longer strips a server from every projects[*]
container in ~/.claude.json: only the top-level entry and the
finding's cold projects lose it, and the cwd's own config files count
as cold only when the cwd is in the cold list. The plan preview
annotates ~/.claude.json with the project entries that lose the
server.
- unused-mcp findings are now appliable (remove-everywhere mcp-remove
plans, same as low-coverage).
- Notes are rendered under manual findings too, so an all-unparseable
config surfaces its parse error instead of a bare "manual".
- ConfigDocs strips a leading UTF-8 BOM before JSON.parse.
- claude-md plans are excluded from --apply --yes (they write to
cwd/CLAUDE.md); they apply via the interactive picker or an explicit
--only selection, and --yes prints them as skipped with the reason.
- --only with an unknown or not-appliable id errors to stderr with the
run's valid appliable ids and exit code 2.
- EOF at the interactive prompt prints "Nothing applied." and exits 0;
an answer that arrives together with EOF is still honored.
Adds end-to-end tests driving runOptimizeApply over injected stdio and
a fixture home: --yes output with journal ids and undo hints, picker
parsing, --only filtering and validation, EOF, claude-md skip, the
projects[*] over-deletion regression, manual-note rendering, and BOM
configs.
Add stable kebab-case finding ids to every optimize detector and to the
JSON report, then route the config-class findings through the action
journal so they can be applied and undone.
- optimize.ts: id on every WasteFinding and OptimizeJsonReport entry;
appliable findings also carry a machine-readable apply payload (mcp
server list, project-scope keepers, archive names).
- act/plans.ts: planFor(finding) builds concrete, journaled file
mutations for mcp-remove, mcp-project-scope, skill/agent/command
archives, CLAUDE.md rule blocks, and the bash output cap. JSON edits
preserve the rest of the document (2-space indent, trailing newline);
unparseable config files are reported and skipped, not fatal.
- act/optimize-apply.ts: codeburn optimize --apply with a plain-readline
confirm, plus --yes, --dry-run, and --only; prints each journal id and
the undo hint. --apply with --json exits 2.
Tests cover mcp remove/undo, project-scope global-to-project move,
unparseable-file skip, archive collision suffixing, CLAUDE.md marker
idempotency, a byte-identical dry-run tree hash, and a finding-id guard.
The archive actions in the acting epic move whole skill/agent directories,
which the framework could not handle: snapshotFile used copyFile (EISDIR on
a directory) and the afterHash pass used readFile. Snapshots now branch on
lstat, copying directory trees with fs.cp recursive. Directories get an
empty afterHash ('' means no content hash) and drift detection skips the
hash comparison for them; the occupied-original-path check still applies and
a missing movedTo still falls back to the backup. Backup restore likewise
branches: a directory snapshot replaces the target (rm then cp recursive).
Apply-side rename cannot replace a directory destination (ENOTEMPTY), so a
move retries once after clearing the already-snapshotted destination,
rethrowing other codes before any destination damage.
Tests: archive a directory tree and restore it byte-identical, move a
directory onto an existing destination directory (destBackup taken, both
trees restored), and dir-move undo with an occupied original path refusing
without --force and overwriting with it.
Undo no longer clobbers files it did not create: an occupied original path
counts as drift for moves (--force removes then renames back), a move
destination that already exists is snapshotted (destBackup) and restored
after the file moves back, and a missing moved file falls back to the source
snapshot so forced undo cannot die mid-loop. Non-move reverts now key on
backup presence instead of the op label, which also restores files that a
create overwrote.
Apply snapshots once per unique path, hashes after all mutations so
overlapping changes carry the final state, and journals inside the rollback
region so a failed append reverts the mutations. The lock is taken with a
single wx write and goes stale by mtime only, so a fresh lock can never be
stolen while empty. Drift reads treat any unreadable target as drift with
its error code, ambiguous id prefixes report the match count, undo --last
skips already-undone records ("Nothing to undo."), and readRecords only
swallows ENOENT.
Tests cover each new behavior plus two mutation probes (forward-order revert
and removed locking both fail the suite), and a CLI-level check of
`act list --json` output shape and ordering.
Add src/act, a dependency-free framework for journaling and reverting any
file CodeBurn modifies. runAction is the single mutation path: it snapshots
every target, applies the changes, then appends a JSONL record, rolling back
completed steps and journaling nothing if a mutation throws midway. Undo
checks each file against its post-apply sha256 and refuses on drift unless
forced, restoring edits, deletes (created files), and moves. A pid plus
timestamp lockfile (stale after 60s) guards apply and undo, and the journal
reader tolerates corrupt lines with last-line-wins status updates.
Wire up `codeburn act list` (table or --json) and
`codeburn act undo <id|--last> [--force]`. Storage lives under the existing
config home via the config.ts resolver. Tests cover apply with backups and
afterHash, byte-identical undo per op type, drift refusal and --force,
mid-apply rollback, and corrupt-journal tolerance.
Recent Cursor builds renamed the per-workspace composer list from
ItemTable['composer.composerData'] to 'composer.composerHeaders' (identical
{ allComposers: [{ composerId }] } shape). loadWorkspaceMap only read the old
key, so on these builds the composer->workspace map came back empty and every
composer fell through to the 'cursor' orphan bucket, losing per-project
attribution.
Read both keys and merge their allComposers lists (backward compatible with
older installs). Add a regression test covering the new composer.composerHeaders
key and the legacy composer.composerData key.
Verified against a real workspace state.vscdb: composer.composerData absent,
composer.composerHeaders present with composerIds matching the bubbleId/
composerData rows; 21/22 calls now attribute to the real workspace instead of
the 'cursor' orphan bucket.
AI-Origin: human
MiniMax moved M3 to tiered pricing: the official standard tier (inputs
up to 512K) is $0.30/$1.20 per M with $0.06 cache reads, doubling above
512K. Upstream LiteLLM now carries the standard tier, so the pinned
expectation follows it; the old $0.60/$2.40 figure is the long-context
tier, not the base price anymore.
Validated against a real thread: request_token_usage is keyed by user
message and covered only part of the requests (cumulative was ~3x the
map sum), so per-request calls are now supplemented by one remainder
entry that brings each thread exactly to cumulative_token_usage. Live
run matches the store token-for-token on all four fields.
Zed's DataType enum carries both zstd (the current save path) and json;
verified against crates/agent/src/db.rs, along with the exact TokenUsage
field names, the SerializedLanguageModel {provider, model} shape, and
RFC3339 updated_at serialization.
Reads Zed's agent threads from the single threads.db SQLite store: each
row's zstd-compressed JSON carries the thread model and per-request
Anthropic-shaped token usage, so calls are emitted per request with
exact input/output/cache fields and priced through the existing engine.
In-progress threads with an empty per-request map fall back to the
cumulative counter. zstd comes from node:zlib, which ships it from
22.15; on older Nodes the provider skips with a notice instead of
failing. On-disk format documented by @chatzinikolakisk in #480 and the
schema verified against a real Zed install.
* fix(cursor): use Cursor's real context tokens for input
Current Cursor builds leave the per-bubble tokenCount at {0,0}, so the provider
fell back to estimating input from visible text plus a second agentKv
content-char pass that double-counted the same conversation. Cursor records its
own tokenizer-accurate context size per conversation in
composerData.promptTokenBreakdown (the number behind the in-app context-window
bar); read that and credit it once per conversation for input instead.
Measured on a real local DB: today's Cursor input went 44,873 -> 168,486 tokens,
matching the sum of per-conversation context. The admin portal still counts
cumulative-per-turn plus cache, which are server-side only, so an opt-in Cursor
API stays the path to exact parity.
Output is a reply-text estimate; agentKv is retained for a tools/bash breakdown
in a follow-up.
* feat(cursor): add tools and bash-command breakdown from agentKv
Cursor logs the agent's tool calls (Read, Grep, Glob, Shell, ...) in agentKv
blobs. Join them to conversations via the turn requestId (carried on the bubble's
$.requestId and inherited positionally by the turn's agentKv rows) and attach each
conversation's tool list and Shell commands to the call that carries its input.
Measured on a real DB: Cursor now reports tools {Read, Grep, Glob, Shell,
SemanticSearch} and the executed shell commands, which were previously empty.
Removes the now-superseded parseAgentKv content-char estimate.
* fix(cursor): price composer-2.5 as Sonnet 4.6
composer-2.5 was missing from the built-in Cursor model aliases, so its usage
showed $0. Map it to claude-sonnet-4-6 like composer-2 (per cursor.com/blog).
* fix(cursor): cache version, model attribution, user message join, tool classification
- Bump CURSOR_CACHE_VERSION to 5: parser semantics changed (parseAgentKv
removed, real context tokens from composerData.promptTokenBreakdown),
stale v4 caches would show double-counted agentKv calls.
- Fix model attribution: real input tokens are credited on user bubbles
(type=1) which carry no modelInfo. Add a pre-pass building composerId ->
model from assistant bubbles so pricing/display uses the conversation's
actual model instead of the default cursor-auto/sonnet-4.5.
- Fix buildUserMessageMap: was keying by JSON conversationId (empty in
current Cursor builds). Now extracts composerId from the bubble key,
matching parseBubbles.
- Add 'Shell' to BASH_TOOLS in classifier: Cursor's agent uses 'Shell'
as the tool name, but it was missing from the bash tool set so Cursor
agent turns with shell commands wouldn't classify as bash/build/test.
- Fix null coalescing in loadComposerInputTokens: r.used ?? r.ctx would
fall through on a valid totalUsedTokens of 0. Use explicit null check.
- Decouple agentTools attachment from input credit: tools/bash were only
attached on the first credited turn (creditedHere), silently dropping
tool usage from subsequent turns in multi-turn conversations.
- Update stale comment about parseAgentKv being kept for a follow-up.
- Add tests for real token crediting, once-per-conversation, fallback,
contextTokensUsed, tool/bash attribution, and model attribution.
* fix(cursor): avoid duplicating aggregated agent tools
* fix(cursor): price house composer models from Cursor's published rates
composer-1/1.5/2/2.5 were proxied to Claude Sonnet, overcounting cost
(~6x for composer-2/2.5). Use Cursor's published per-model rates instead,
and note in the parser why local reads undercount the admin console.
Co-authored-by: AgentSeal <hello@agentseal.org>
* fix(cursor): estimate non-Composer turn input from the agent stream
Non-Composer sessions (e.g. GPT) record no context-window meter and keep
the prompt in the agent stream, so the user bubble's own text is empty.
Those turns hit the 0/0-token fallback with text_length 0 and were dropped
entirely, so that model's traffic never appeared in the report.
loadAgentToolsByComposer now also sums the user-role stream text length per
conversation, and the meterless fallback estimates input from it (chars/4),
credited once per conversation, when the bubble text is empty. Turns with no
stream text are left untouched, so no phantom tokens are invented.
* fix(cursor): stable conversation crediting, restored stream coverage, and cache invalidation
Review fixes for the real-token accounting:
Conversation input now lands on one composer-anchored record
(cursor:composer-input:<id>) timestamped at composerData.createdAt, so
the credited day no longer depends on the parse window or cache floors,
daily-cache gap fills dedupe instead of multiplying, and each
conversation picks exactly one input source (real bubble tokenCounts,
the context meter, the agent stream, or visible text) so sources can
never stack or double count. A zero totalUsedTokens no longer shadows
contextTokensUsed.
The agent stream regained what the parseAgentKv removal dropped: tool
and system rows count as context, stream-only replies count as output,
and sessions with no bubble join are emitted again (DB mtime timestamp,
as before). Block-array content is measured by its text, not its JSON
envelope. Rows written before their requestId appears buffer forward
instead of inheriting the previous conversation, and a system row closes
the boundary. Tool names canonicalize to Bash and commands go through
extractBashCommands so cross-provider breakdowns merge; the classifier
no longer special-cases Shell (which also reclassified Copilot turns).
User bubbles consume their own queue entry so assistant replies pair
with the right question, and every cursor call is flagged
costIsEstimated.
The requestId and model joins ride the existing budgeted bubble scan
instead of two new unbounded full-table decodes, and the composerData
read seeks the key range. SQLITE_BUSY now propagates to the parser's
retry path instead of caching a silently degraded parse.
Upgrades actually take effect: the session cache gets a cursor parse
version, DAILY_CACHE_VERSION bumps to 10 so finalized days re-hydrate
under the new accounting, the cursor results cache bumps to v6, and the
builtin composer rates participate in the price config hash (rates now
cite cursor.com/docs/models).
Verified against a real Cursor store: all metered conversations match
the on-disk meter exactly, narrow and wide parse windows anchor
identically, repeat runs are byte-identical, and agentKv-only sessions
reappear.
---------
Co-authored-by: ozymandiashh <234437643+ozymandiashh@users.noreply.github.com>
* fix(devin): report friendly GPT model names with effort tier (#487)
Devin transcripts label each step with a generation_model in dash form with
an effort suffix (gpt-5-3-codex-xhigh). getShortModelName is keyed in dot form
(gpt-5.3-codex), and its version-boundary check matches the dash id against the
base gpt-5 entry, collapsing every Devin GPT variant to GPT-5.
Normalize the dash form to canonical dot form, map through the short-name table,
and surface the effort tier, e.g. GPT-5.3 Codex (xhigh). Fall back to the
friendly model_name when generation_model is an opaque MODEL_* id, and read
extra.generation_model so ATIF v1.7 transcripts behave like v1.4. Devin rows
in the JSON report and model-efficiency now key on the friendly name.
* fix(devin): stop dated GPT snapshots being mislabeled as versions
The friendly-name path treated any two-number dash id (gpt-4-1106-preview) as a
dotted minor version, producing corrupt labels like 'GPT-4.1106 Preview'.
- Restrict the dash-to-dot rewrite to a single-digit minor at a token boundary,
matching Devin's real ids (gpt-5-3-codex) while leaving dated snapshots alone.
- In getFriendlyGptName, defer to getShortModelName when any suffix token is
purely numeric, so unknown snapshot ids pass through raw instead of being
fabricated into a fake friendly name.
Adds a regression case (gpt-4-1106-preview) to the Devin variant matrix.
* chore: remove accidental node_modules symlink
An absolute-path node_modules symlink was committed by mistake. It leaked a
local username, was a dangling symlink for everyone else, and broke git/npm on
checkout (git won't replace a real node_modules/ with the symlink). It slipped
past .gitignore because the ignore rule is 'node_modules/' with a trailing
slash, which matches a directory but not a symlink.
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.
The dashboard Cache read/write cards and the `codeburn devices` summarizer
summed the full 365-day history.daily backfill instead of the selected period,
so shorter windows over-counted (today ~197x on real data). Every other metric
already reads the period-scoped `current` block.
buildMenubarPayload now emits period-scoped cacheReadTokens/cacheWriteTokens on
`current` (from PeriodData, alongside inputTokens/outputTokens); the web cards
(single + combined views) and summarizeOneDevice read those instead of reducing
history.daily. The trend chart still uses history.daily. Older peers that omit
the fields fall back to 0 (web) or the windowed daily sum (CLI).
Adds regression tests for both surfaces.
Adds a per-(provider, model) audit table showing the raw token fields each
provider records (input, output, reasoning, cache write/read) alongside the
totals codeburn displays and prices. It makes the normalizations explicit:
reasoning folds into output, the Anthropic cacheReadInput / OpenAI cached
vocabularies collapse to a per-call max, and cache write/read use the 1.25x /
0.1x input multipliers when a model omits explicit cache rates.
--format json adds per-component cost, the applied rates, both raw cache-read
fields, and recomputed vs attributed cost, so any token-to-cost drift is
visible in one command. Same options as `codeburn models`.
The web server is long-lived, so cache what the per-invocation CLI cannot:
- Cache the parsed local payload in-memory (single-flight, 180s TTL matched to
the parser session cache, expired entries pruned on write). /api/usage and the
local half of /api/devices now return from a Map hit after the first parse.
- Prewarm today at startup and inline that payload into index.html as a
bootstrap, so the SPA paints today's numbers with no round-trip. Only the
local device is embedded; '<' is escaped so a name cannot break the script
tag; served no-store; the seeded view refetches at once so live peers appear.
- Default the web command and dashboard to today.
- Cap the peer connect phase at 3s. req.setTimeout does not abort a stalled TCP
connect, so an offline paired device hung ~75s on the OS timeout; it now
degrades to an unreachable row in ~3s. The cap clears on TCP connect, so the
TLS handshake and the 65s pairing-approval wait are unaffected.
Measured: /api/usage 0.0007s warm (was ~0.22s), /api/devices ~3s with an offline
peer (was ~75s), first paint instant.
* feat(menubar): expose combined multi-device usage in menubar-json (#566)
The macOS menubar reads `codeburn status --format menubar-json`, which only
ever reflected the local machine. Multi-device "Combined" usage existed only in
the terminal `devices` command. This exposes combined + per-device usage through
the menubar-json contract so the menubar can mirror the combined dashboard.
- Extract the per-device summary + combined reduce that was inline in
renderDevices into a reusable pure `summarizeDeviceUsage(results, window?)`.
Error/unreachable devices are excluded from the combined sums (kept in
perDevice); deviceCount counts all, reachableCount counts the reachable ones.
renderDevices now formats from it with byte-identical output.
- Add an optional `combined?: CombinedUsage` block to MenubarPayload (perDevice
list + combined totals incl. calls/sessions). Absent by default.
- Add `--scope local|combined` to `status` (default local). `combined` builds the
local payload, pulls paired devices (pullDevices isolates per-peer failures),
and attaches the summary.
- Correctness guards: reject `--scope combined` with `--days` (non-contiguous,
not representable over the sharing query) and with `--provider`/`--project`/
`--exclude` (the sharing query carries no filters, so peers would report
unfiltered usage). Window-scope the cache-token sum to the selected period
(cache lives in 365-day daily history; current carries no cache counts).
TS/CLI only. The menubar Local/Combined toggle + render is a follow-up.
* fix(menubar): never let combined enrichment break the base local payload
The status --format menubar-json --scope combined path pulls paired devices.
Wrap that best-effort enrichment in a guard so an unexpected failure (corrupt
remotes store, peer I/O) can never take down the base local menubar payload —
on error the local payload is still emitted with combined omitted. Add a test
that a corrupt remote-devices.json still yields a valid combined (local-only) payload.
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
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.
The Skills & Agents panel was always empty for OpenCode sessions even
when skills and subagents were used. buildAssistantCall (shared by the
OpenCode SQLite and file-based parsers, and Kilo Code) counted the
skill/task tool invocations but never read the skill name or subagent
type from the tool-call input, so the dedicated breakdowns had no names
to aggregate.
In OpenCode's part files the identifier lives in state.input: the skill
tool carries input.name and the task tool carries input.subagent_type.
Extract both in buildAssistantCall and surface them on
ParsedProviderCall.skills / .subagentTypes, then stop hard-coding
skills: [] in providerCallToTurn and providerCallToCachedCall so the
classifier's subCategory and the subagent breakdown receive the data.
Verified against real OpenCode data: the previously empty skills[] and
subagents[] now populate (pipeline-investigation, plan-spec, splunk, ...
and explore/general subagents).
Fixes#556
Co-authored-by: Kevin Addison <kevin.addison3@tesco.com>
* fix(web): reject invalid dashboard periods without exiting
* test(web): assert invalid periods return 400 without exiting; drop redundant /api/devices re-parse
- Add tests/web-dashboard.test.ts: boots the dashboard on an ephemeral port and asserts
/api/usage and /api/devices answer 400 (not process.exit) for a bad period, and that
the server keeps serving afterward. runWebDashboard now returns the http.Server so it
can be exercised in-process; callers that ignore the return value are unaffected.
- /api/devices: resolve periodInfo once instead of validating then re-parsing it inside
localGetUsage (pullDevices invokes localGetUsage with the same already-validated query).
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
* 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>
The 2GB stream cap silently dropped whole Codex session files over the limit, so a live 2.4GB session (about 308M tokens of real gpt-5.5 spend) was excluded and reported as zero. Raise MAX_STREAM_SESSION_FILE_BYTES to 4GB (the line-by-line reader is bounded-memory and handles multi-GB files), and surface any remaining oversize skip via an always-on notice instead of a verbose-gated warning so a dropped session is never silent. Adds a maxBytes option for testability and tests covering the cap boundary.
Two post-0.9.12 cleanups.
Report model names: getShortModelName resolves a model's pricing alias before looking up its display name, so models priced via a sibling alias (ZCode/Hermes GLM-5.2 via glm-5p1, Grok Build via grok-build-0.1) leaked the internal pricing key as the model name in report --format json and the menubar model breakdown. Grok Composer was unmapped and showed raw. Add SHORT_NAMES entries so each resolves to its real name (GLM-5.2, Grok Build, Grok Composer 2.5 Fast). The models command was already correct because it uses each provider's own modelDisplayName.
Daily cache: providers added since the v8 rollup (Grok, Hermes, ZCode) parse usage that older binaries skipped, so days cached at v8 omit them and report 0 across history. Bump DAILY_CACHE_VERSION and MIN_SUPPORTED_VERSION to 9 to force a one-time full re-hydration so new providers backfill without a manual cache clear.
* fix(antigravity): read current agy antigravity-cli on-disk layout
* fix(antigravity): propagate SQLITE_BUSY from the .db read so the run retries
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
* fix(cursor-agent): ingest workspace-less CLI transcript layout
* fix(cursor-agent): bump parse version so cached sessions pick up the new ingestion
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
gpt-5.3-codex-spark is a distinct model variant, but the longest-first
startsWith(key+'-') matcher (intended for reasoning suffixes -high/-low)
swept it into the base 'GPT-5.3 Codex' label, making a distinct model look
like the retired base in today/models/status output. Add an explicit
display entry in SHORT_NAMES and the codex provider so the longer key wins;
-high/-low still fall through to the base. Pricing is unaffected (LiteLLM
has no spark entry; cost falls back to the base rate as before).
Fixes#461
- Read the populated sessions.cwd column for project grouping; fall back to
scraping the transcript, then the profile name. The current Hermes build no
longer writes "Current working directory:" lines into messages.
- Set costIsEstimated when the figure is the LiteLLM fallback (no recorded cost).
- Re-throw SQLITE_BUSY from the discovery and read paths so a transient lock on
the live state.db is retried instead of being cached as an empty result.
- Tests: add the cwd column to fixtures; cover cwd grouping, the estimated flag,
and tool-result tool_name extraction.
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.
The 'By Project' panel collapsed sibling Claude Code projects to a parent
folder in two cases:
1) resolveCanonicalProjectPath walked up to ANY ancestor .git and used it as
the canonical project. A stray .git in a parent of several projects (a
dotfiles bare repo, an accidental git init) made every sibling resolve to
that parent ('Projects'/'home'). Now only a real linked worktree (.git is a
FILE pointing at <main>/.git/worktrees/<name>) canonicalizes to its main
repo; an ordinary repo or a stray ancestor .git keeps the session's own cwd
as the project. Genuine worktree grouping is preserved.
2) When a Claude session has no cwd metadata, the dir slug was unsanitized by
replacing every '-' with '/', inventing path segments that overview then
split on, so 'Projects-Content-OS' rendered as 'os'. The lossy slug is now
kept intact, and overview only basenames ABSOLUTE paths.
Fixes#493
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
* feat(overview): cache in/out tokens table, roomier tables
Add a Tokens breakdown table (Input / Output / Cache in / Cache out /
Total with share %) so cache write/read are shown clearly instead of
crammed into the Totals line, and widen table cell padding to 2 spaces so
columns aren't congested.
* feat(overview): full comma-grouped numbers for a precise, spacious look
Render token counts as full thousands-grouped integers (e.g.
3,926,923,819) instead of abbreviated M/B, so the roomy tables read like
a precise statement. Update the overview test to match.
* feat(web): device discovery + pairing endpoints for the dashboard
Add /api/identity, /api/devices/scan (mDNS browse with confirm code and
paired status), and /api/devices/pair (approve-style pairing via
linkRemote). Backs the Search local devices button so pairing can happen
from the browser instead of the CLI.
* feat(web): two-panel dashboard with eywa-inspired warm theme
Redesign the dashboard into a left sidebar (device switcher + Search
local devices) and a right content panel, restyled to a warm-paper,
forest-green archival theme (serif display numbers, ink text, hairline
borders) in place of the dark midnight theme. The Search local devices
modal discovers nearby devices over mDNS and pairs in one click (approve
on the other device), backed by /api/devices/scan and /api/devices/pair.
* feat(web): use the CodeBurn flame logo in the dashboard header
Replace the placeholder triangle with the flame mark (the repo's
codeburn-symbolic vector), tinted the theme's forest green, and set the
same flame as the favicon.
* feat(web): color the All-devices chart by device, add task-level combined views
In the All devices view the daily chart now stacks by device (one color
per device) instead of by model, and the combined panels add a By task
breakdown plus in/out token detail. Devices that cannot be reached are
hidden entirely rather than shown as error rows.
* feat(web): use the flame image as the dashboard logo and favicon
Replace the inline vector flame with the brand flame PNG
(public/codeburn-flame.png) in the header and as the favicon.
* fix(web): trim flame logo padding and tighten brand spacing
Crop the transparent border off the flame PNG (it was 184px of padding
each side) and reduce the header gap so the mark sits close to the
wordmark.
* feat(web): cost/tokens toggle, cache read/write, full feature panels
Add a Cost/Tokens unit toggle that switches the hero number and the chart
between dollars and tokens. Surface cache write/read token totals as
metric cards. Add per-device panels for subagents, skills, MCP servers,
and a savings / retry-tax / routing-waste summary. The combined view
gains cache totals and the unit toggle too.
* feat(web): use CodeBurn website logos and add community links
Use the website's navbar flame (logo.png) with the Code+Burn wordmark in
the header, and the three-flame app icon (icon.png) as the favicon.
Add Website, Discord, and X links to the sidebar footer.
* fix(web): use the single-flame logo for the favicon too
The three-flame icon was wrong for the tab; use the same single flame as
the navbar everywhere and drop the unused three-flame asset.
* chore(web): set dashboard title to CodeBurn - Local Dashboard
* harden sharing + dashboard for public launch
Security:
- pairing: cap PIN attempts (close window after 5 wrong guesses) so a
6-digit PIN cannot be brute-forced within the TTL on a 0.0.0.0 listener.
- web dashboard: reject non-loopback Host (defeats DNS rebinding that
could read unsanitized local usage) and cross-origin requests (CSRF);
require application/json on the pair endpoint.
- store tokens with 0600 perms (was world-readable), 0700 dir.
Robustness:
- client: per-request timeout so a hung/asleep peer cannot hang a pull;
pullDevices fetches remotes concurrently and isolates failures.
- dashboard: normalize peer payloads at the boundary and add an error
boundary so a peer on a different version cannot white-screen the SPA;
finite-guard fmtNum/compactUsd.
Tests: PIN attempt-cap test added (1269 pass).
* feat(web): share this device from the dashboard, with browser approval
Add a Share this device toggle to the dashboard sidebar. It runs the
secure share server in-process (mTLS + mDNS advertise) so no terminal is
needed, with a Keep sharing always option (persisted; otherwise it stops
after idle). Incoming approve-style pairings are queued and surfaced in
the browser as an Approve/Deny prompt with the matching code, instead of
a terminal prompt. The shared payload is sanitized; start degrades
gracefully if the port is already held by a CLI share.
* fix(sharing): keep a paired device from dropping on a transient pull
- client: 8s -> 15s timeout and a fresh socket per request (agent:false) so
the pinned-fingerprint check always reads this connection's cert.
- share server: wrap request handling so a getUsage error returns a fast
500 instead of hanging the caller (which times out and drops the device).
- dashboard: re-pull paired devices every 20s so a brief drop self-heals
instead of staying gone until you change tabs.
* fix(sharing): re-sanitize remote payloads on receipt
A peer might run an older build that does not strip its own project
names/sessions. Sanitize every remote payload when we receive it too, so
project names never cross into our dashboard regardless of the sender's
version. Aggregate numbers (cost, tokens, models, tools, daily) are kept.
* harden dashboard sharing: version-skew, lifecycle, server errors
- normalize remote daily-history entries so a peer on an older build (daily
rows missing topModels) can no longer crash the chart / brick the page.
- ShareController: commit always/sharing state only after listen() binds, so
a port-in-use start no longer reports sharing when it is not.
- attach durable error/tlsClientError handlers to both HTTPS servers so a
malformed peer connection cannot crash the process.
- reset view/provider selection when the viewed device disappears or lacks
the selected provider (no more empty/no-selection state on sleep-wake).
- by-device chart: stable per-device color/key so bars do not reshuffle when
a device drops or returns between polls.
* polish: device identity, approval guard, queue cap, formatting
- give each device a stable unique id (cert fingerprint for remotes,
'local' for this device); key the sidebar, selection, and by-device
chart by id so two devices sharing a hostname no longer collide.
- approval prompt: drop a request from the UI the moment it is answered
so it cannot be double-clicked.
- share controller: cap concurrent pending approvals and allow only one
per device, so a LAN peer cannot flood the prompt.
- usd(): render negatives as -$5.00, consistent with compactUsd.
* feat(sharing): pairing, token, and device-identity core
First piece of local device sharing: self-cert fingerprint identity
(trust-on-first-use), a one-time expiring pairing PIN, fingerprint-bound
tokens, and a peer store that authorizes a pull only when both the token
and the TLS peer fingerprint match the same paired device. Pure logic,
fully unit-tested; the TLS share server and host pull build on this.
* feat(sharing): secure mutual-TLS transport + pairing handshake
Add device identity (self-signed cert, persisted; fingerprint = sha256 of
the cert DER), an HTTPS share server (mutual TLS: presents its cert, reads
the client's, and serves /api/usage only when the bearer token AND the
client fingerprint match the same paired peer), a one-time-PIN pairing
endpoint, and a fingerprint-pinning client. Verified end to end on
loopback: PIN pairing, pinned authed pull, and rejection of a wrong PIN,
a token replayed from another device, and a mismatched server
fingerprint. Adds the selfsigned dep (Node cannot generate certs natively).
* feat(sharing): share + devices CLI (pair, pull, combine)
Phase 3 terminal flow: codeburn share runs the secure server on-demand
(stops after 10 min idle; --always to persist, --pair to add a device),
and codeburn devices add <host> --pin <pin> pairs and pins a remote.
codeburn devices pulls this machine plus every paired device, keeps each
separate, and prints a per-device table with a simple summed Combined
row (no server-side merge). Persists identity and peers under the config
dir. Host pair-and-pull flow covered by a loopback integration test.
* feat(sharing): mDNS discovery + approve-style (no-PIN) pairing
Add bonjour-service discovery (advertise/browse over the LAN), a short
confirmation code derived from both cert fingerprints (Bluetooth-style
'do these match?' check), and an approve-style pairing endpoint that
prompts the owner instead of requiring a typed PIN. Loopback-tested:
approved device gets a working token with a matching code on both sides,
declined device is rejected.
* feat(sharing): AirDrop-style discover + approve UX
codeburn share now advertises on the LAN and approves incoming devices
interactively (confirm the matching code, no typed PIN). codeburn devices
add (no args) discovers nearby devices, lets you pick one, shows the
confirmation code, and waits for the owner to approve. Manual
add <host> --pin stays as a fallback for networks that block mDNS.
* feat(sharing): share only aggregates, never project names or paths
Sanitize each device's payload before it leaves the machine: drop
topProjects and topSessions (project names + session detail) and send
only aggregate numbers (cost, tokens, models, tools, activities, daily).
What you are working on stays local; only the totals travel.
* fix: fix and improve test isolation and collision with environment
* docs: remove unnecessary comment
* test(env-isolation): clear CODEBURN_FORCE_MACOS_MAJOR and pin TZ
Two env vars read in src/ were not isolated: CODEBURN_FORCE_MACOS_MAJOR
(now cleared so it cannot leak between tests) and TZ (now pinned to UTC,
since clearing it falls back to the OS zone and would shift date buckets
versus a clean CI runner).
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
* feat(overview): plain-text monthly usage overview command
Add 'codeburn overview', a copy-pasteable text report. Defaults to the
current month, with --from/--to to filter and --no-color for plain
output. Renders Totals, By tool, Top models, Highest-value days, Top
projects, a daily table, By activity, and Tools, with colored section
headings (stripped under --no-color or when piped).
Reuses the existing session aggregation. Cost gains thousands
separators and tokens roll up to a B unit for readability via
display-only wrappers, without changing the shared formatters. Project
names use the path basename instead of the sanitized full path.
* fix(cli): exit cleanly on EPIPE when the stdout pipe closes early
Piping output to a reader that closes early (| head, quitting less, a
missing command) made stdout writes throw EPIPE and crash with an
unhandled error event. Handle EPIPE on process.stdout and exit 0 so
piping the overview and other commands behaves normally.
* docs(readme): document and highlight the overview command
Add a 'Your month at a glance' section featuring codeburn overview with
examples and sample output, a Commands-table entry, and the provider
flag note.
OpenCode 1.1+ stores sessions as file-based JSON under
storage/{session,message,part}/ instead of a SQLite DB, so the
SQLite-only provider discovered zero sessions on current installs and
reported no usage at all.
Add a file-based discovery and parser path, preferred when present and
falling back to the SQLite DB for pre-migration installs. Extract the
shared message-to-call logic (token extraction, tool and bash parsing,
cost) into session-message.ts so the file path, the SQLite path, and
Kilo Code stay identical.
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.
The export schema emitted tools and shell-commands but no MCP section, and
the tools list is built from extractCoreTools which strips mcp__ names, so
MCP usage never appeared in codeburn export output even when sessions had
MCP activity (issue #496).
Add an mcp section to the JSON export and an mcp.csv to the CSV export,
sourced from session.mcpBreakdown (server -> calls, with share). Mirrors the
existing tools section.
The underlying parse fix (Codex mcp_tool_call_end attribution) landed in
#513; this makes that data visible in exports. Validated on real local data:
the JSON export now reports mcp: [{Server: node_repl, Calls: 5, ...}].
Fixes#496