mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-25 16:19:39 +00:00
11 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6dc81f5ce6
|
fix: four cost accuracy bugs in export and pricing (#1544)
Some checks are pending
CI / semgrep (push) Waiting to run
Tests / test-platforms (windows-latest) (push) Waiting to run
Tests / desktop (push) Waiting to run
Tests / test (22) (push) Waiting to run
Tests / test (22.13.0) (push) Waiting to run
Tests / test (24) (push) Waiting to run
Tests / test (26) (push) Waiting to run
Tests / test-platforms (macos-latest) (push) Waiting to run
* fix: four cost accuracy bugs in export and pricing - export daily rows bucket each call on its own day (same rule as the day aggregator) and no longer skip a turn without a timestamp - cache-write tokens on a non-Anthropic model with no published write rate bill at the input rate instead of a fabricated 1.25x (calculateCost and audit); daily cache v35 and pricing schema 4 so stored figures re-derive - OpenCode/Kilo messages with output but no usage, and Copilot transcript calls with no token count, are marked estimated - export records keep unrounded cost and gain estimated/unpriced columns * fix: plain-decimal CSV costs, re-parse cached copilot transcripts for the estimated flag * ci(upgrade-path): compare copilot records at cent precision across the export rounding change |
||
|
|
2fc62f7876
|
0.9.25: correctness, energy, menu bar and installer fixes (#1511)
* 0.9.25: correctness + provider fixes, energy guard, logo, default DMG CLI/engine fixes: local-model savings count reasoning; ZCode reasoning billed; codex-credits version-boundary tiering; GLM alias repointed to z.ai discounted rate; openclaw/pi/qwen timestamp mtime fallback; hermes delta timestamp; sqlite/warp/hermes busy+permission reclassified as failed (no sealed empty); codex-cache dev+ino; cursor-cache WAL; antigravity server TTL; quota timeout+ grokbot footer+claude near-expiry; serve RSS guard env+hysteresis; CSV export subfolder; providers pane fixed window; Warp credit-based cost. Desktop/menubar: unified flame logo; default DMG; docs signed+notarized. * menubar: one running instance, self-relaunch on language change The single-instance guard only retired strictly older copies, so two login items starting in the same instant both survived, and a copy whose bundle identity did not survive install was never seen at all. The decision is now total (newest launch wins, ties break on pid) and peers are matched on bundle id or executable name. --keep-both opts one launch out. Language changes no longer have the desktop app quit and reopen the menu bar, which made macOS attribute the new process to the desktop app and re-ask for the data-access grant. The menu bar relaunches itself via the existing remote command key; older menu bars that do not watch the key fall back to the old path after 5s. * desktop: export toast shows the saved path; resource buckets on app_close The export toast showed the folder that was picked, not the dated subfolder the files land in. The handler already parsed the CLI's saved line; it now returns that path and the toast uses it. app_close carries cpu/mem (and serveCpu/serveMem when serve reported) as bucket labels, sampled on the existing flush interval. serve adds cpuSec and peak rssMb to its answer frames. Unmeasured figures are omitted, never sent as zero. * menubar guard never leaves zero instances; installer replaces in place; JSON export into folder Guard: an unreadable launch date on either side could make two copies retire each other. Unreadable peers now count only when their pid is lower, and a copy that cannot read its own date retires lower pids only, so a disagreement keeps at least one instance. Installer: an existing bundle in /Applications or ~/Applications is replaced where it lives instead of a second copy being added. The old bundle is moved aside and restored on any failure; a cross-volume move falls back to a copy that is re-verified with codesign. An unwritable location is never escalated: the install goes to ~/Applications and the leftover is named. Export: -o pointing at an existing folder writes the JSON inside it. Short sessions omit the cpu buckets. setLanguage reopens the menu bar if its own relaunch never comes back. AppRelaunch passes the bundle path as an argument. * menubar guard orders by kernel start time; installer opens the copy that exists and recovers a killed install Guard: launch dates could be unreadable and pid order can disagree with start order after pid reuse, so two copies could still retire each other. Ordering now uses the kernel-reported process start time, which both sides read identically. A copy whose start time cannot be read is left out, so the worst case is two menu bars, never none. Installer: with the only copy in an unwritable /Applications, a plain run tried to open a path that did not exist. It now opens the installed copy. A placement killed mid-way is recovered on the next run, a concurrent install is refused, and a failed restore names where the previous bundle is. Process matching uses the exact executable name. Export: a trailing separator names a folder. Telemetry cpu is measured from telemetry start. * installer: a recycled pid can no longer block installs; language change waits for the replacement process An aside left by an earlier install counted as an active install whenever its pid was live, so once that pid was reused every install was refused for good. It now also has to be recent (ctime within 10 minutes), a pid we may not signal is not ours, the refusal names the file, and an orphan that cannot be cleared is reported instead of aborting the install. setLanguage confirmed the relaunch with a matcher the dying process still satisfied, so the reopen fallback never fired. It now waits for a pid that was not running before. * desktop: only the headline refreshes live; act, yield and the optimize scan move to slower tiers An open window ran act report (a fresh CLI process each time), yield and an optimize scan on every refresh. The headline poll now passes --no-optimize and takes the snapshot path; act report refreshes on mount, on manual refresh and every 10 minutes, yield every 5. The optimize scan runs at most once a day per query scope, is stored in userData with its time, and is forced fresh by the Optimize page or a manual refresh. Figures that are not live show their age, and no stored scan means no savings clause rather than a zero. On battery the live interval doubles; the chosen cadence stays the base. * desktop: a stored optimize scan is only valid on the day it was computed; cadence changes no longer refetch The scan cache is keyed on the argv, which says 'today' or '30days', not a date, so yesterday's scan was served as today's. It is now reused only on the same local day. Stored findings in Signals carry their age, a failed scan shows the error instead of scanning forever, and the cold-boot label uses the time the app received the payload. usePolled fetched immediately whenever its interval changed, so every power transition (and launching on battery) refetched every live hook and could spawn an extra act report. Fetching and timer arming are now separate effects. Optimize's yield is on the same 5 minute tier as Overview. * desktop: re-ask for the optimize scan on the first render of a new local day An app left open across midnight kept yesterday's stored savings under Today, because the scan was only requested on mount, scope change or manual refresh. The local day is now one of the hook's triggers; the previous day's figure is dropped while the new scan runs. * serve: a menubar poll is never answered from the status snapshot Inside its settle window the status snapshot returns the pre-change payload on purpose. The resident serve process then memoized that answer as clean, so after a single write followed by quiet the headline stayed on the old figure until the next write or the five minute memo cap. The snapshot is for one-shot runs; the resident process already holds the parsed state, so it now skips it. * tests stop writing the real serve pid file; superseded caches are swept; installer notes reach the desktop card ServeConnection takes its pid file as a parameter, so the Swift tests no longer share ~/.cache/codeburn/menubar-serve.pid with a running menu bar, and the reaper only signals a record that is a serve --stdio command. Superseded session-cache, codex-results and cursor-results files (100MB+ each) are removed once untouched for 14 days, at most once a day. daily-cache and the hermes ledger are never swept: they carry sealed days a version bump adopts. The menu bar installer's leftover-copy advice is emitted as a marked line under CODEBURN_PROGRESS and shown on the desktop card. The skipped-refresh NSLog is debug only. The app's vitest workers turn off Node's built-in web storage. * menubar: anonymous usage statistics, under the same consent as the desktop and the Windows tray The Swift menu bar sent nothing, so every codeburn-menubar row was a Windows tray. It now follows the desktop's telemetry.v1.json when that exists (read only: same install id, same on/off, onboarded required) and otherwise keeps its own state with the same region default (off in EU/EEA/UK/CH and when the region is unknown). Settings gains one Privacy toggle, disabled when the desktop owns the choice. Events use the tray's names; the daily usage snapshot is the CLI's block passed through a sanitizer and is left to the desktop when it is the consent source. Debug builds never send. A resident CLI that gets disabled for the run now says so in the log. * menubar statistics: private network session, abandoned batch on opt-out, a desktop No survives the desktop going away The sender used URLSession.shared, so a server or CDN cookie and the default User-Agent rode along and would have outlived an opt-out. It now owns an ephemeral session with no cookies, no cache, a fixed agent string and no redirects. A batch that is out when the toggle goes off is dropped rather than re-queued under the fresh id. The desktop's answer is recorded locally, so a missing or damaged state file never falls back to the region default, and the standalone id is persisted when minted. The toggle stays usable as a local off switch under desktop consent. Copy states what this app actually sends. * vercel gateway spend is shown on its own row and kept out of totals by default The gateway reports daily aggregates per model, with nothing to match against the local tools that were pointed at it, so every headline counted that traffic twice. Totals now leave the gateway slice out when all providers are shown; the row keeps its full amount and is marked not in total, --provider vercel-gateway is unchanged, and codeburn gateway-totals include folds it back in. The daily cache still stores the slice, so the choice applies retroactively. Reports with no gateway credential are byte-identical. The gateway's request_count now feeds the call count instead of each daily row counting as one call. Hermes: a mid-session price change lands on the day it is observed and sealed days are never rewritten; stated in the code and changelog. * gateway exclusion applies to every report through the parse, and the detail panels follow the headline The first pass narrowed only the day totals, so By tool, top models, top projects, activities, status json projects and any project-filtered table still carried gateway spend next to a headline that did not, and By tool shares could pass 100%. The rule now lives in parseAllSessions: with all providers in scope and the opt-in off, aggregate-only sessions are dropped after the memo, and the callers that derive cached days opt out so stored slices are unchanged. day.projects is reduced like models. The gateway stays visible once, as a labelled not-in-total row with no share, and standalone reports note the excluded amount on stderr so machine output is unchanged. * dashboard: the background history index follows the gateway exclusion too The index keeps the whole corpus so it can fill the daily cache, but its per-period projection handed that unfiltered list to the project, model and activity panels, so they regained gateway spend once the index landed. * ci: a hung lock suite fails in 90s and retries once with a warning; slow Windows tests get measured limits The cache-lock step failed 6 of the last 60 runs with no output for five minutes after the vitest banner; passing runs take 12-18s on every Node version. Each attempt is now capped at 90s and retried once, with a warning whenever that happens. On windows-latest a single CLI spawn takes 1.4-12s, so the four-spawn status snapshot test and the c6 parser suite get limits taken from those runs, each with its reason. Both test jobs print their ten slowest cases. * desktop: Privacy & data uses the same rows as the rest of Settings The pane mixed two static claims with two controls in its own layout. It now uses the about-row pattern every other pane uses: title and hint on the left, one control on the right. The claims became the subtitle. Rows: anonymous usage statistics (toggle, link to what is sent), device sharing state with Manage, local report snapshots with Clear, export with Open export. * desktop: Privacy & data keeps one bounded column and rows that never squeeze The card stretched with the window, stranding each control hundreds of pixels from its text. This pane's heading, subtitle and card now share a 760px column and one text measure. Rows stack on their own width, because the window can never be narrow enough for the old viewport rule to fire; row buttons do not wrap. The what-is-sent link sits on its own line, and the snapshot and export descriptions fit their measure and say this machine. * menubar tests clean up their scratch defaults and leave the real Keychain alone Four test files never removed their UserDefaults suites and the rest removed the domain but left the plist, so thousands of files piled up in ~/Library/Preferences. A shared helper forgets a suite and deletes its file, and every test that makes a suite now uses it. cfprefsd still rewrites an empty plist when the test process exits, so a run leaves about a hundred 42-byte files with no test data in them. The real-Keychain round trip only runs with CODEBURN_TEST_REAL_KEYCHAIN=1. * desktop: the resident parser retires after 15 idle minutes and restarts on demand The renderer stops polling while the window is hidden, but the serve child stayed resident with its parsed cache, one to two GB on a large corpus. After 15 minutes without a request it is stopped the way a settings restart stops it, without spending the unexpected-death budget, and the next request starts a new resident through the existing lazy path, never a one-shot. CODEBURN_SERVE_IDLE_MS tunes or disables it. * desktop: every settings pane keeps a bounded column, and dark buttons have an edge Cards stretched with the window, so on a wide display controls sat up to 1300px from their labels and intro lines ran past 140 characters. All panes now share one left-aligned column (760px, 980px for projects, aliases and pricing, which hold wide rows) and one text measure. Rows stack on their own width instead of a viewport rule the 900px minimum window could never reach, and a long path in Projects no longer overflows the pane. In dark mode the secondary button matched or sat below its surface, and was invisible on the plugin install dialog. It gets a face and a border from two new tokens; light is unchanged. The onboarding consent label matches Settings. * desktop: this device shows its fingerprint on its own line The local name and the fingerprint are two adjacent inline spans, so they read as one run of text. * menubar: the resident parser retires after 15 idle minutes and restarts on demand The menu bar kept its serve child, and the parsed cache with it, for the whole run, even with the popover closed and nothing changing. After 15 minutes without a request the child is retired without counting as a death, and the next request starts a new resident through ensureStarted, never the one-shot fallback, whose snapshot settle window would make the figure lag a whole tick. The window sits above the longest gap between real requests, so a child stays warm while work is happening. CodeBurnServeIdleSeconds tunes or disables it. * serve: a poll re-checks only what the watcher saw change, and shard writes are coalesced With a session being written, every poll re-listed about 2,400 directories and re-fingerprinted about 6,000 transcripts, then rewrote the whole current-month shard (49MB on a large corpus) and read it back on the next poll. The resident process now reuses the previous listing and fingerprints for paths its root watcher proves untouched, and falls back to the full sweep whenever the watcher is missing, unhealthy, overflowed, armed after the sweep, does not cover the path, the memo is older than 4.5 minutes, or the process was suspended. Shard publication is held for up to 90s and flushed when stdin closes; a held write is dropped rather than written over something another process published. One-shot runs are unchanged. Active poll cost 0.71 to 0.33 CPU-s at a 30s cadence; time-to-reflect unchanged at about 1.2s across append, new dir, new file, delete, rename-over, in-flight, watcher overflow and unhealthy watcher. Changelog: state the telemetry default as it is in this version. * a retired parser gets to publish its held shards, and durable providers are never held Both apps retired the serve child with SIGTERM before its stdin-close flush could run, so every retire discarded the coalesced window. They now close stdin, give the child a bounded grace to publish and exit, and signal it only if it is still running; a settings restart does the same. Measured: flushed 0/5 before, 5/5 after, for both clients. copilot's cache entries are the only record once Copilot prunes its own files, so a held window that was dropped (retire, crash, kill -9) before the source was pruned lost that spend for good. A window that a durable provider dirtied is published on the poll that parsed it, as before coalescing existed. The RSS guard publishes a held window before it clears the caches instead of discarding it. * serve: release the lifetime parse after the fill, and make the collection real The RSS guard dropped its memos and then called globalThis.gc, which exists only under --expose-gc; no launcher passes it, so the collection had never run and the freed pages never went back. On a cold start the resident idled at 2.2GB with 364MB of live heap: clearing every memo moved RSS by 0MB, a collection alone by 700MB. After the background fill has sealed its summaries the process now publishes any held shard window, drops the memos it can re-read from disk (the cold start had loaded the cache with scope 'all', which then served every later poll), and collects; it collects again a minute later for as long as a pass returns 64MB, at most four times. Cold idle 2198MB to 700MB, warm 900MB to 781MB. Reports are leaf-identical cold and warm, time-to-reflect unchanged, a released month scope reloads in 0.3-0.5s. * warp: the billed cost reaches reports and survives the cache The provider derived a dollar cost from Warp's own billing record (provider cents, charged usage, or credits), but warp was missing from the reported-cost pass-through, so the cached call kept no cost and every report, cold or warm, priced the conversation from its tokens. A fixture billed at $1.22 reported $0.15. A call whose cost came from a billing record now keeps it; a call that only has the token floor stores none and is still re-priced when prices change. Warp entries re-parse once (provider parse version), and the daily cache moves to v34 so days whose Warp source still exists re-derive; call counts do not move, so nothing is double counted or dropped. * openclaw and pi: a reported cost survives the cache like every other reported cost Both providers read the cost their tool recorded for a message, and both were missing from the reported-cost pass-through, so reports priced every call from tokens instead. Fixtures reported $0.011 against $0.056 (openclaw) and $0.013 against $0.410 (pi). A call with a reported cost now keeps it; a call with none, or an explicit zero, stores none and is still re-priced when prices change. omp had been added to the list in #1150 and pi, the same parser, was never revisited. The provider list is one named set, the unreachable providerCallToTurn is gone, and both providers re-parse once; the v34 daily cache re-derives their days with call counts unchanged. * test: pin parse count per report and per resident poll |
||
|
|
a5a0ddaeac
|
feat(copilot): read per-request input/cache from session-store.db
The Copilot CLI and the GitHub Copilot desktop app both write ~/.copilot/session-store.db unconditionally; its assistant_usage_events table holds one row per API request. Until now input/cache tokens for these surfaces came only from the session.shutdown rollups in events.jsonl, which are written only on clean shutdown (a crash loses the whole leg's input/cache accounting) and lump each session leg into one per-model total. The rollup also RESETS its counters at in-session compaction (traced on a clean single-process 107-request session whose sole rollup covered exactly its five post-compaction requests), so even cleanly-closed long sessions were truncated; on a long-history machine the store recovered ~35% of real Copilot spend lost to crashes and compaction resets. The DB rows are per-request, crash-proof, and carry real timestamps. The store's input_tokens is cache-INCLUSIVE (input + cache_read + cache_write), the same convention as the shutdown rollups — verified against each row's token_details_json and by reconciling per-session sums against the CLI's own footers and rollups across two machines (1,380+ rows, 8 models, CLI 1.0.70–1.0.79, schema_version 6): every divergence was a rollup gap. Emitted calls mirror the shutdown-call contract: input/cache/reasoning only, output 0 — per-turn output stays owned by the events.jsonl assistant.message calls. Rollup-vs-store precedence is RECONCILED at serve time, per (session, model), and only there. Both representations always parse and cache; parseProviderSources aggregates the cached calls and, wherever store rows exist for a (session, model), drops the rollup calls and serves the rows plus per-leg RESIDUAL calls: each rollup leg subtracts only the rows in its own interval — rows commit strictly before their leg's shutdown line, so a leg at time T covers exactly the rows in (previous leg's T, T] — and any remainder (per token component, floored at zero) serves once at that leg's own timestamp. A store missing requests a leg covered — adopted mid-session, rows pruned before ever being read — therefore still serves that tail exactly once ON THAT LEG'S DAY, a crash-tail row the rollup never saw can never cancel it, and a complete store serves pure per-request granularity with every residual retired to zero. The decision reads only cached contents, never discovery: deleting or resetting the store changes nothing served, so finalized daily history can never flip on an absence epoch; cached rows of a deleted store remain the record until the 90-day orphan age-out (which exempts still-discovered paths). The serve set is the one coherent snapshot — nothing a writer does between discovery and a parse can change what one pass sees — and read-time precedence heals persisted duplication (stale epochs, runtimes without node:sqlite, restored files) instead of preserving it, following the buildDurablePeriod pattern. Store rows and rollups carry supplementary accounting weight. A rollup (or its residual) is aggregate accounting, never a request: zero api-call/model-call/turn weight, tokens and cost fully retained. A store row is one real request, but when it pairs with a served per-turn call it is supplementary too; rows pair with same-model per-turn calls by timestamp adjacency (monotone matching, tight 2-minute window — the two are written at the same completion moment, and a wide window would let a crash-only row pair against a neighbor whose own row is missing), computed once over the FULL serve set so a date-range boundary that separates a row from its call cannot double the request across adjacent day queries. Only the unpaired rows — store-only requests, exactly where crash-lost requests sit — count. Supplementary-only turns fold into the nearest behavioral turn within 30 minutes; with no behavioral turn to fold into they stay separate weightless turns, each on its own day, with apiCalls 0 — and the session emission gate admits usage-bearing zero-call sessions. The weight propagates into the daily cache: aggregateProjectsIntoDays applies the same rule to every calls counter and category-turn count it seals, so v19 history and live summaries can never disagree about what was a request. A changed source whose read defers on the busy shape (locked, EACCES, corrupt mid-replace — discovery still emits the source; only true absence or a schema mismatch reads as absent) now marks session hydration incomplete, so the daily backfill holds its watermark instead of finalizing a day the deferred rows never reached; an unchanged unreadable store defers nothing. The verdict travels with its result — the 180s memo and the serve burst-reuse restore the hydration verdict their cached data was parsed under, so a memoized partial parse cannot inherit a later parse's complete — and a discovered source whose FINGERPRINT cannot be read (EACCES on a present file) defers instead of silently skipping, while a genuinely deleted file stays a silent skip. Copilot reasoning tokens are no longer double-billed at the report layer: they are a subset of the output the per-turn calls already price, and copilot joins claude in the reasoning-inside-output case of the query-time cost recompute. Store dedup keys are content-discriminated — copilot-store:<sid>:<rowId>:<fnv1a64(created_at|tokens|model)> — because AUTOINCREMENT prevents id reuse only within one database lifetime: a same-path DB reset reusing row ids now mints new keys instead of the durable union swallowing the new usage, while a byte-identical re-insert still collapses (64-bit: 32-bit FNV collisions between plausible token tuples are constructible). Every call of a session serves under one project label resolved at serve time — the session-state-derived label when the serve set knows it, else the store rows' own — so neither rows cached before events.jsonl existed nor an events.jsonl orphaned by a session-state prune can split the session across two grouping keys. CODEBURN_COPILOT_SESSION_STORE_DB is read but deliberately NOT fingerprinted, per the #927 ruling (any copilot fingerprint change drops cached entries whose path still exists, destroying pruned history only the cache holds); the read is allowlisted in the #927 guard, and serve-time reconciliation makes repointing safe without a fingerprint — the new store's rows parse on sight and the old path's entries persist as durable orphans. The copilot parse version appends session-store-v2 and the daily cache bumps v17 → v19: per-day attribution, call counts and costs all change against pre-store builds. 19, not 18: an earlier pushed head of this PR already claimed v18 under different accounting, and the carry-forward would adopt those days as finalized without re-deriving them. Verified by A/B on snapshots of two real stores, a live SIGKILL crash test (row present, no rollup, tokens recovered exactly), live resumes whose warm-cache deltas matched new rows to the token, upgrade-healing at 4,800-session scale, and serve-level regressions pinning every maintainer finding from six review rounds: the rows-then-shutdown race, stale-cache healing, age-out exemption, absence-epoch identity, progressive row landing with residual retirement, behavioral weight across all four pinned scenarios, the hydration fence, project unification in both directions, the same-path reset, mixed coverage (crash tail vs covered-leg gap), multi-leg residual day attribution, range-invariant pairing, memo-scoped hydration verdicts, and the fingerprint-failure fence. |
||
|
|
a55251ce8f
|
feat(export): subagentType and model on per-record exports; --by-agent follow-ups (#712)
Follow-ups to #710, completing issue #708's second ask plus the two review notes: - export: new records.csv with one row per call incl. subagentType and model; sessions.csv gains both fields APPENDED after all legacy columns so by-index consumers are unaffected; JSON fields additive; formula- injection escaping preserved - models --by-agent: the main-session bucket sentinel becomes '(main)' - agentType is free-form text, so a real subagent literally named 'main' no longer merges into the main bucket (test-pinned across all formats) - README: note that the --min-cost 0.01 default can hide sub-cent agent buckets; --min-cost 0 shows the complete agent inventory |
||
|
|
808a149bd6
|
feat(export): surface MCP server usage in JSON and CSV exports (#514)
Some checks are pending
CI / semgrep (push) Waiting to run
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 |
||
|
|
be6068b244
|
feat(report): add per-model efficiency metrics
Adds per-model efficiency metrics (edit turns, one-shot rate, retries/edit, cost/edit) to the TUI By Model panel, JSON report output, and CSV export. Closes item 4 of #12. Supersedes #226 with review fixes (units rename, min-sample guard in TUI, tighter <synthetic> filter, multi-model attribution test). Original implementation by @ozymandiashh. |
||
|
|
fc4c4f0091
|
feat(export): support custom date ranges | ||
|
|
c16b21ec50 |
fix(classifier): surface skill name as subCategory for general turns (#203)
Turns whose only assistant tool is `Skill` collapse to category `general`
because `classifyByToolPattern` returns `'general'` and `refineByKeywords`
only operates on `coding`/`exploration`. In environments that lean on Claude
Code skills, the per-activity dashboard column flattens — every `/init`,
`/review`, `/security-review`, `/claude-api`, plus user-defined skills, all
land in `general` with no signal about which workflow ran.
Implements Option A from the issue:
- `ParsedApiCall.skills: string[]` populated in the Anthropic-path parser
via a new `extractSkillNames` helper that reads `input.skill || input.name`
from each `Skill` ToolUseBlock (mirrors `detectGhostSkills` extraction at
optimize.ts:765 so the two stay in sync).
- `ClassifiedTurn.subCategory?: string` set to the first skill name when the
resolved category is `general` AND any skill identifier was extracted.
Top-level category stays `general` — existing aggregations, exports, and
category-keyed code paths unchanged.
- `SessionSummary.skillBreakdown: Record<string, {turns,costUSD,editTurns,
oneShotTurns}>` populated in the same per-turn loop that builds
`categoryBreakdown`. Provider sessions (Codex/Cursor/etc.) keep `skills:
[]` — they don't expose the Skill tool surface today.
- Dashboard `ActivityBreakdown` renders top-N skill sub-rows beneath the
`general` row when present (indented `/skill-name`, dimmed). Other
categories render exactly as before; if no skills were invoked, the panel
is byte-identical to current output.
Existing 419 tests still pass. New `tests/classifier.test.ts` adds 8 cases:
single skill via `input.skill`, single via `input.name`, first-wins for
multi-skill turns, aggregation across multiple assistant calls in one turn,
no-name fallback (`subCategory` stays undefined), `Skill+Edit` promoting to
`coding` and dropping subCategory, non-Skill general turns, and a legacy
ParsedApiCall shape with `skills` field absent (forward-compat). Pre-fix
verification by stashing the source change reproduces 4/8 failures with the
exact "expected 'init', received undefined" diff; restoring → 8/8 pass.
Closes #203.
🤖 AI assistance disclosure: assistant-scaffolded by Claude (Opus 4.7);
author of record reviewed every line, ran the full vitest suite locally
(`npm test` → 32 files / 427 tests pass), `npx tsc --noEmit` clean, and
`npm run build` produces a clean ESM bundle.
|
||
|
|
a4d261a536 |
fix: pricing accuracy, stream leak, CSV injection hardening
- Remove bidirectional fuzzy match in getModelCosts that could return wrong pricing when a short canonical name prefix-matched a longer key - Use explicit undefined check in parseLiteLLMEntry so free models with zero cost are not silently dropped from the LiteLLM pricing database - Destroy read stream in finally block of readSessionLines to prevent file descriptor leaks when the generator is abandoned early - Extend CSV injection escaping to cover tab and carriage-return prefixes - Add optional chaining fallback for empty periods in exportCsv/exportJson - Add regression tests for all fixes (models, export, fs-utils) |
||
|
|
495a254338 |
feat(mac): native Swift menubar app + one-command install
Introduces mac/ with a native SwiftUI menubar app that replaces the previous SwiftBar plugin entirely. Install via `npx codeburn menubar`, which downloads the .app from GitHub Releases, strips Gatekeeper quarantine, and drops it into ~/Applications. Highlights - mac/ SwiftUI app: agent tabs, Today/7/30/Month/All period switcher, Trend/Forecast/Pulse/Stats/Plan insights, activity + model breakdowns, optimize findings, CSV/JSON export, Star-on-GitHub banner, live 60s refresh, instant currency switching with offline FX cache. - Security: CodeburnCLI argv-based spawn (no shell interpretation), SafeFile symlink guards + O_NOFOLLOW writes, FX rate clamping to [0.0001, 1_000_000], keychain filtered to account == "default", removed byte-window credential log, in-flight refresh guard, POSIX flock on config.json writes, TerminalLauncher validates argv before AppleScript interpolation. - Performance: shared static NumberFormatter (thousands of allocations per popover redraw eliminated), concurrent pipe drain with 20 MB cap + 60s timeout in DataClient, Observation-tracked reactive UI, 5-min payload cache keyed on (period, provider). - CLI: new `codeburn menubar` subcommand that downloads + installs + launches the .app (no clone, no build). New `status --format menubar-json` payload builder. `export` rewritten to produce a folder of one-table-per-file CSVs with a `.codeburn-export` marker so arbitrary -o paths cannot be silently deleted. - Removed: src/menubar.ts (SwiftBar plugin generator), install-menubar / uninstall-menubar subcommands, `status --format menubar` directive output, tests/menubar.test.ts, tests/security/menubar-injection.test.ts. - Release: .github/workflows/release-menubar.yml builds universal binary, assembles .app, ad-hoc signs, zips, uploads on mac-v* tag push. Runs on the free macos-latest runner. Tests - 230 TypeScript tests pass - 10 Swift CapacityEstimator tests pass - TypeScript typecheck clean - Swift release build clean |
||
|
|
9ab7f37f6f | Fix CSV formula injection in exports |