Users who run MCP tools through a CLI wrapper (e.g. philschmid/mcp-cli)
instead of registering servers natively don't produce Codex
mcp_tool_call_end events; Codex logs a plain exec_command. So their MCP
usage showed only as a shell command and was absent from the MCP
breakdown, even after #513 fixed the native path.
- Recognize `mcp-cli [options] call <server> <tool>` in the exec command
and also attribute it as mcp__<server>__<tool>. The exec still counts as
Bash since it genuinely is a shell exec. Flag-tolerant, quote/path/bash-lc
tolerant, and scoped to the mcp-cli binary (not foo-mcp-cli); only the
`call` subcommand (a real execution) matches, not info/grep/listing.
- Register `codex` in PROVIDER_PARSE_VERSIONS. session-cache.json serves
unchanged session files without invoking the provider parser, so the
codex-cache version bump alone would never re-attribute already-cached
sessions. This also retroactively repairs #513's native-MCP fix for
users whose files were cached before it shipped.
- Bump CODEX_CACHE_VERSION 4->5 for the provider-internal layer.
Tested: parse cases (bash -lc wrapper, flags-before-call, argv array,
plus info/grep/foo-mcp-cli negatives), and a mutation-verified cache
regression test that fails without the PROVIDER_PARSE_VERSIONS entry.
ReDoS-checked. Reviewed with a Fable 5 adversarial pass (Codex is down);
its two must-fixes (the cache gap and the flags-before-call miss that
would have missed the reporter's own sessions) are folded in.
Fixes#478
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
Recent Codex sessions report completed MCP tool calls as an event_msg with
type mcp_tool_call_end instead of a function_call response_item. The parser
only built its tool list from function_call and patch_apply_end events, so
these MCP calls were never attributed: token usage was still counted, but the
MCP tool itself was missing from recent lookback windows (today/week/month)
while older history still showed it.
Add a handler that reads invocation.server and invocation.tool from the
event and rebuilds the canonical mcp__<server>__<tool> name the classifier
recognizes. Bump the Codex result cache to v4 so sessions cached under v3
re-parse and pick up the previously dropped MCP calls.
Validated on real local data: 5 mcp_tool_call_end events that were dropped
before are now attributed as mcp__node_repl__js, with no change to the total
call count.
Fixes#478
Forked Codex sessions copy the parent's entire token_count history
(re-timestamped), so those replays are keyed into the parent's namespace
(forkedFromId) and deduped to avoid double-counting the parent's spend. But the
key used cumulativeTotal alone, which is too coarse: a genuine post-divergence
fork event whose running total coincidentally equals some parent total collided
with it and was dropped, undercounting real spend.
Add the cumulative token breakdown (input, cached, output, reasoning) to the
dedupe key. A fork replays those cumulative figures verbatim, so a true replay
still collides and parent and fork are not double-counted, while genuinely
different work that happens to reach the same cumulative total stays distinct.
The breakdown must come from the CUMULATIVE totals, not the per-event deltas:
the deltas are computed against a running `prev` that the fork advances
differently once the 5s replay cutoff skips some events, so a delta-based key
would spuriously diverge on a replay and double-count it (in the total-only
fallback branch).
This is the correct fix for issue #413. The reported "undercount" is mostly
replayed parent history being correctly credited to the parent, not lost; the
issue's suggested change (key on the fork's own session id) would instead
re-double-count the entire replayed history. Three regression tests pin the
invariants: a replay past the 5s cutoff counts once, a divergent fork event
sharing a parent's cumulative total is kept, and a total-only fork whose replay
straddles the cutoff does not overcount.
Two passes of validators across CLI accuracy, dashboard UX, menubar Swift,
performance, security, and end-to-end smoke tests on real session data.
Data-correctness fixes:
- parseLocalDate rejects month/day overflow. JS Date silently rolled
Feb 31 to Mar 3, so --from 2026-02-31 --to 2026-03-15 quietly dropped
sessions on Feb 28 - Mar 2. Now throws "Invalid date" with a clear
reason. Leap-day case covered (2024-02-29 valid, 2025-02-29 rejected).
- CSV/JSON exports use the active currency's natural decimal places. The
previous round2 helper produced ¥412.37 in CSV while the dashboard
rendered ¥412 — finance teams comparing the two surfaces saw a
discrepancy. New roundForActiveCurrency consults Intl.NumberFormat for
the right precision (0 for JPY/KRW/CLP, 2 for USD/EUR, etc).
- Copilot toolRequests is Array.isArray-guarded in both modern and legacy
event branches. Previously a corrupt session with toolRequests=null or
a string aborted the whole file's parse loop and silently dropped every
legitimate call after it.
- Codex token_count dedup uses a null sentinel for prevCumulativeTotal so
the first event is never confused with a duplicate. Sessions that emit
only last_token_usage (no total_token_usage) report cumulativeTotal=0
on every event; with the previous 0-initialized prev, the first event
matched the dedup guard and was dropped.
- LiteLLM pricing values are clamped to [0, 1] per token via safePerTokenRate.
Defense in depth against a tampered upstream JSON shipping negative or
absurdly large per-token costs that would otherwise propagate into all
cost totals.
Performance:
- Cursor SQLite parse no longer pegs at minutes on multi-GB DBs. Two
changes: per-conversation user-message buffer uses an index pointer
instead of Array.shift() (which was O(n) per call); and a real ROWID
cutoff via subquery limits the scan to the most recent 250k bubbles
with a stderr warning so power users get a partial report rather than
a stalled CLI.
- Spawned codeburn CLI subprocesses are terminated when the calling Task
is cancelled. Without this, rapid period/provider tab clicks in the
menubar cancelled the Task but left the subprocess running to
completion, piling up zombie processes.
UX:
- Dashboard period switch flips to loading and clears projects
synchronously before reloadData runs, eliminating the frame where the
new period label rendered over the old period's projects.
- Optimize findings tab paginates 3-at-a-time with j/k scroll. With 4
new detectors plus 7 originals, 8-10 findings * 6 lines was scrolling
the StatusBar off the alt buffer top.
- Custom --from/--to ranges hide the period tab strip and disable the
1-5 / arrow keys so a stray period press no longer abandons the user's
explicit range. A "Custom range: X to Y" banner replaces the tab strip.
- OpenCode storage-format warning is per-table-set, rate-limited to once
per process, and points the user at OpenCode's migration step or the
issue tracker. The previous all-or-nothing check fired the generic
"format not recognized" string for any schema mismatch.
Menubar / OAuth:
- Both Claude and Codex bootstrap (Reconnect button) now honour the
usageBlockedUntil 429 backoff that refreshIfBootstrapped respects.
Spamming Reconnect during sustained rate-limit windows previously
hammered the upstream endpoint on every click.
- Codex Retry-After HTTP header is parsed (delta-seconds plus IMF-fixdate
fallback) so we don't over-back-off when ChatGPT tells us a shorter
window than our 5-minute floor.
- Both credential cache files are written via SafeFile.write
(O_CREAT | O_EXCL | O_NOFOLLOW with explicit 0600) so there is no race
window where the temp file briefly exists at default umask, and a
symlink at the destination cannot redirect the write. Reads now route
through SafeFile.read with a 64 KiB cap, closing the symlink-follow gap
on Data(contentsOf:).
CI signal:
- TypeScript strict typecheck (tsc --noEmit) is now zero errors. The
six errors in src/providers/copilot.ts came from a discriminated-union
catch-all branch whose `data: Record<string, unknown>` shape TS picked
over the specific event branches when narrowing on `type`. Removed the
catch-all; runtime falls through unknown event types via the existing
if/else chain.
Tests added: 16 new (now 555 total)
- date-range-filter: month/day/year overflow rejection, leap-day correctness
- currency-rounding: convertCost no-rounding contract, roundForActiveCurrency
for USD/JPY/KRW/EUR
- providers/copilot: malformed toolRequests does not abort the parse
- providers/cursor-bubble-dedup: re-parse after token mutation does not
double-count, single parse yields one call per bubble
- providers/codex: first event with cumulativeTotal=0 not dropped,
consecutive zero-cumulative duplicates still deduped
- Stop tracking a separate streamError flag. createReadStream's default
64 KiB highWaterMark means the stream may already be reading chunk 2
when we break out of the loop after yielding the first line; if that
later chunk errors, the flag could reject an otherwise-valid line.
readline's async iterator already re-throws stream errors on Node 16+,
which the existing catch handles.
- Test: 120 KB session_meta line forces multi-chunk line assembly.
- Test: truncated mid-write first line is rejected, not parsed as half
an object.
- Cap createReadStream at 1 MiB so a malformed file with no newline
cannot make readline buffer indefinitely (real session_meta lines
are 22-27 KB).
- Capture stream errors explicitly; readline's async iterator does
not always re-throw underlying stream errors per Node docs.
- Test: assert project is extracted from the >16 KB session_meta to
prove the line was actually parsed, not just discovered.
- Test: session_meta line with no trailing newline is still accepted.
- Test: empty rollout file is silently skipped.
`readFirstLine` allocated a fixed 16 KB buffer, but Codex CLI 0.128+
embeds the entire base_instructions / system prompt in the
`session_meta` line, pushing it past 20 KB. When the buffer doesn't
catch a newline, `isValidCodexSession` rejects the session, so every
recent Codex session is silently excluded from totals.
Switch to a streaming readline read so the first line is captured
regardless of length, and add a regression test that creates a
40 KB session_meta payload.
Locally, this changes my 30-day Codex total from €267 (only ~half
of sessions parsed) to €878 (all sessions parsed).
Codex Desktop on Windows uses "Codex Desktop" as the originator
string instead of "codex_cli" or "codex_vscode". The startsWith
check was case-sensitive, rejecting these sessions silently.
Fixes#1 (comment by @JiglioNero).
- Remove agent-*.jsonl exclusion filter that was dropping ~46% of API calls
- Scan subagents/ directories for subagent session files
- Normalize Codex token semantics: OpenAI includes cached tokens inside
input_tokens, subtract them to match Anthropic's separate reporting
- Fixes cost double-counting and 100% cache hit display for Codex users