Skip to content

fix(codebuddy): collect traces as authoritative token source, fix 90% undercounting - #399

Open
HandSonic wants to merge 6 commits into
xiufengsun:mainfrom
HandSonic:fix/codebuddy-traces-collection
Open

fix(codebuddy): collect traces as authoritative token source, fix 90% undercounting#399
HandSonic wants to merge 6 commits into
xiufengsun:mainfrom
HandSonic:fix/codebuddy-traces-collection

Conversation

@HandSonic

@HandSonic HandSonic commented Jul 31, 2026

Copy link
Copy Markdown

Summary

CodeBuddy CLI writes authoritative token usage to ~/.codebuddy/traces/<pid>/trace_*.json (and ~/.workbuddy/traces/ for WorkBuddy). Each trace JSON carries trace.modelInfo.{totalInputTokens, totalOutputTokens, totalCachedTokens, callCount} with 100% field stability (verified on 286 real traces).

The existing jsonl reader only sees ~10% of messages — 90% of assistant messages in newer CodeBuddy versions (glm-5.2) lack providerData.rawUsage in ~/.codebuddy/projects/**/*.jsonl. This causes massive undercounting.

Additionally, the extension-log path overlaps with the jsonl path using a different dedup key namespace, causing 3x inflation when both are active.

Root cause

  1. Traces dir not scannedresolveCodebuddyProjectFiles only scanned projects/ and extension logs, never traces/
  2. 90% of jsonl messages lack rawUsage — newer CodeBuddy (glm-5.2) stops writing providerData.rawUsage on most assistant messages; token data only exists in traces
  3. Extension-log double-counting — jsonl and extension-log use different dedup keys (entry.uuid vs codebuddy:extension-log:agentId:sec), so the same LLM round-trip is counted twice
  4. WorkBuddy rawUsage fallback missing — older WorkBuddy sessions only write providerData.model without rawUsage; some carry the camelCase top-level usage field instead

Fix

  1. resolveCodebuddyProjectFiles: scan traces/ dir, return {path, kind} objects (jsonl | trace | log)
  2. parseCodebuddyIncremental: add trace JSON branch — read modelInfo, dedup by traceId, skip jsonl messages for traced sessions (tracedSessionIds) to avoid double-counting
  3. resolveWorkbuddyProjectFiles: same traces/ scan (WorkBuddy is the renamed successor of CodeBuddy, same trace schema)
  4. parseWorkbuddyIncremental: same trace JSON branch
  5. WorkBuddy rawUsage fallback: when providerData.rawUsage is absent, fall back to top-level usage field (camelCase convention)
  6. Extension-log parsing: default disabled (TOKENTRACKER_CODEBUDDY_LOG_FALLBACK=1 to enable) — overlaps with jsonl, different dedup namespace, causes 3x inflation
  7. File sort order: trace files sorted before jsonl files so tracedSessionIds is populated before jsonl parsing (prevents double-counting sessions with both trace and jsonl)
  8. Reasoning tokens: read from completion_tokens_details (not prompt_tokens_details), subtract from completionTokens to avoid double-counting
  9. WorkBuddy sqlite fallback: fix total_tokens = inputTokenstotal_tokens = input + output + cacheRead + cacheCreation + reasoning (pre-existing bug in main)

Token math (trace branch)

input_tokens        = modelInfo.totalInputTokens - modelInfo.totalCachedTokens
cached_input_tokens = modelInfo.totalCachedTokens
output_tokens       = modelInfo.totalOutputTokens
total_tokens        = input + cached + output  (= modelInfo.totalInputTokens + totalOutputTokens)

This matches the jsonl branch convention (cached is separate from input).

Scope

  • CLI (src/lib/rollout.js)
  • Tests (test/codebuddy-traces.test.js, test/rollout-parser.test.js)

Verification (real machine, 2026-07-31)

Metric Before After
codebuddy 7/31 total 15.3M (jsonl only, 90% missing) 129.5M
codebuddy 7/31 (traces alone) 103.8M (modelInfo.input + output)
10-day total (codebuddy + workbuddy) 386M
Double-counting 45.9M (3x inflated) eliminated (tracedSessionIds dedup)

Tests

  • test/codebuddy-traces.test.js: 3/3 pass (trace discovery, idempotent re-parse, trace+jsonl dedup)
  • test/rollout-parser.test.js: 230 pass / 1 fail ([bug]: Zed Agent的用量无法统计到 #154 pre-existing, unrelated)
  • test/install-resolver.test.js: 28/28 pass
  • test/dual-install-integration.test.js: 1/1 pass
  • test/codex-wsl-shadow.test.js: 1/1 pass
  • No regression in existing tests

Commits (6, based on main)

Commit Description
db578b1 Main fix: traces collection + workbuddy rawUsage fallback + extension-log opt-out
fe52757 Test adaptation for {path, kind} return type
cfd0eac Fix reasoning double-count, provider null pointer, trace sort order
5039510 Fix CODEBUDDY_HOME cleanup typo, ?? nullish fallback
8f1be2c WorkBuddy provider.model/messageId optional chaining
6016d91 WorkBuddy sqlite fallback total_tokens formula fix (pre-existing bug)

CodeRabbit review

All actionable comments addressed:

  • ✅ reasoning double-count (completion_details + subtract)
  • ✅ provider null pointer (optional chaining throughout)
  • ✅ file sort order (trace before jsonl)
  • ✅ trace-specific tests added
  • ✅ CODEBUDDY_HOME cleanup typo
  • ||?? nullish fallback
  • ✅ Pre-existing sqlite fallback bug fixed

Checklist

  • Commits follow conventional style (fix:, test:)
  • PR description explains why, not just what
  • Verified against authoritative data (traces dir)
  • All CodeRabbit actionable comments resolved
  • Tests added for new functionality (trace discovery, dedup, idempotency)

Note

This PR is independent of #397 (codex WSL shadowing fix). Both touch src/lib/rollout.js but in different functions (codebuddy/workbuddy collectors vs codex sources builder). If #397 merges first, this should rebase cleanly.

CodeBuddy CLI writes authoritative token usage to
~/.codebuddy/traces/<pid>/trace_*.json (and ~/.workbuddy/traces/ for
WorkBuddy). Each trace JSON carries trace.modelInfo.{totalInputTokens,
totalOutputTokens,totalCachedTokens,callCount} with 100% field stability
(verified on 286 real traces). The existing jsonl reader only sees
~10% of messages (90% lack providerData.rawUsage in newer CodeBuddy
versions), causing massive undercounting.

This change:
1. resolveCodebuddyProjectFiles: scan traces/ dir, return {path, kind}
   objects (jsonl | trace | log)
2. parseCodebuddyIncremental: add trace JSON branch — read modelInfo,
   dedup by traceId, skip jsonl messages for traced sessions
   (tracedSessionIds) to avoid double-counting
3. resolveWorkbuddyProjectFiles: same traces/ scan
4. parseWorkbuddyIncremental: same trace JSON branch
5. workbuddy rawUsage fallback: when providerData.rawUsage is absent,
   fall back to top-level usage field (camelCase convention)
6. Extension-log parsing: default disabled (TOKENTRACKER_CODEBUDDY_LOG_FALLBACK=1
   to enable) — it overlaps with jsonl and uses a different dedup key
   namespace, causing 3x inflation

Verified on real machine (2026-07-31):
- Before: 15.3M (jsonl only, 90% missing) or 45.9M (3x inflated)
- After: 129.5M (traces + non-traced jsonl sessions)
- traces alone: 103.8M (modelInfo.input + output, includes cache)
- 10-day total: 386M tokens across codebuddy + workbuddy traces
@HandSonic
HandSonic requested a review from xiufengsun as a code owner July 31, 2026 09:07
@github-actions github-actions Bot added the cli label Jul 31, 2026
Comment thread src/lib/rollout.js

let traceObj;
try {
const raw = fssync.readFileSync(filePath, "utf8");
Comment thread src/lib/rollout.js

let traceObj;
try {
const raw = fssync.readFileSync(filePath, "utf8");
…nd} return type

resolveCodebuddyProjectFiles and resolveWorkbuddyProjectFiles now return
{path, kind} objects (jsonl | trace | log) instead of bare strings, to
support the new trace JSON branch. Update the two affected tests to map
entries to .path before asserting.
@github-actions github-actions Bot added the tests label Jul 31, 2026
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9940c4a6-f964-42a9-a5c5-11530c407d52

📥 Commits

Reviewing files that changed from the base of the PR and between 8f1be2c and 6016d91.

📒 Files selected for processing (1)
  • src/lib/rollout.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/rollout.js

📝 Walkthrough

Walkthrough

CodeBuddy and WorkBuddy discovery now returns typed file entries and includes trace JSON files. Both parsers aggregate trace usage, support newer usage fields, deduplicate trace data, and persist bounded state. CodeBuddy log fallback is opt-in.

Changes

Trace ingestion

Layer / File(s) Summary
CodeBuddy trace discovery and aggregation
src/lib/rollout.js, test/codebuddy-traces.test.js
CodeBuddy discovers typed trace entries, reads trace totals, aggregates token usage, tracks covered sessions, and persists bounded deduplication state. Tests cover discovery, incremental deduplication, and JSONL suppression.
WorkBuddy trace and JSONL ingestion
src/lib/rollout.js, test/rollout-parser.test.js
WorkBuddy discovers and parses trace files, deduplicates trace IDs, supports usage formats, handles missing provider metadata, includes all SQLite token components, and preserves nested subagent resolver checks.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: xiufengsun

Sequence Diagram(s)

sequenceDiagram
  participant CodeBuddyDiscovery
  participant CodeBuddyParser
  participant TraceJSON
  participant DedupState
  CodeBuddyDiscovery->>CodeBuddyParser: provide typed trace entries
  CodeBuddyParser->>TraceJSON: read finalized trace totals
  TraceJSON-->>CodeBuddyParser: return model and token usage
  CodeBuddyParser->>DedupState: check and persist trace and session state
Loading
sequenceDiagram
  participant WorkBuddyDiscovery
  participant WorkBuddyParser
  participant TraceJSON
  participant DedupState
  WorkBuddyDiscovery->>WorkBuddyParser: provide typed trace entries
  WorkBuddyParser->>TraceJSON: read trace totals
  TraceJSON-->>WorkBuddyParser: return normalized token usage
  WorkBuddyParser->>DedupState: check and persist trace IDs
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main CodeBuddy trace collection change and the resulting token undercount fix.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
test/rollout-parser.test.js (1)

7079-7082: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add tests for trace discovery and trace aggregation.

The resolvers now emit kind: "trace" entries, and the cursor schema gained seenTraceIds and tracedSessionIds. No test creates a traces/<pid>/trace_*.json file. Add cases that:

  • assert the resolver returns a trace entry for traces/<pid>/trace_*.json;
  • run the parser twice over the same trace file and assert the second run aggregates nothing;
  • assert a session with both a trace file and a JSONL file is counted once.

The third case directly covers the double-count risks raised in src/lib/rollout.js.

Based on the coding guideline: "Run parser and synchronization tests, including consecutive sync runs after changing sync.js or cursor schema, to detect state pollution."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/rollout-parser.test.js` around lines 7079 - 7082, Add tests in the
rollout parser suite for trace handling: create a traces/<pid>/trace_*.json
fixture and assert the resolver returns an entry with kind "trace"; parse the
same trace twice and verify the second run adds nothing; and combine that trace
with a session JSONL file to assert the session is counted only once. Include
cursor state fields seenTraceIds and tracedSessionIds as needed, and cover
consecutive parser runs without state pollution.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/rollout.js`:
- Line 7978: The rollout aggregation currently counts trace and JSONL totals for
the same session. In src/lib/rollout.js lines 7978-7978, update the file sort to
place kind: "trace" entries before kind: "jsonl" entries. In
parseWorkbuddyIncremental at src/lib/rollout.js lines 8727-8797, maintain a
persisted tracedSessionIds set, add each trace.sessionId in the trace branch,
and skip JSONL usage records whose session IDs are already tracked.
- Around line 8828-8832: Update both WorkBuddy provider accesses in the loop
around the messageId check and model usage to use optional chaining, matching
the CodeBuddy branch’s safe access pattern. Preserve the topUsage fallback while
ensuring records with usage but no providerData do not throw when evaluating
provider.messageId or provider.model.
- Around line 8387-8411: Update the CodeBuddy usage extraction around rawUsage
and topUsage so reasoning tokens come from completion/output details rather than
prompt/input details. Subtract reasoningTokens from completionTokens before
assigning the billable output_tokens value, and avoid adding those tokens again
through reasoning_output_tokens; preserve the five-column billing model and
existing fallback behavior.

---

Nitpick comments:
In `@test/rollout-parser.test.js`:
- Around line 7079-7082: Add tests in the rollout parser suite for trace
handling: create a traces/<pid>/trace_*.json fixture and assert the resolver
returns an entry with kind "trace"; parse the same trace twice and verify the
second run adds nothing; and combine that trace with a session JSONL file to
assert the session is counted only once. Include cursor state fields
seenTraceIds and tracedSessionIds as needed, and cover consecutive parser runs
without state pollution.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 098b7987-72e1-46ff-943f-d8d3028754e5

📥 Commits

Reviewing files that changed from the base of the PR and between 739e759 and fe52757.

📒 Files selected for processing (2)
  • src/lib/rollout.js
  • test/rollout-parser.test.js

Comment thread src/lib/rollout.js Outdated
}

files.sort((a, b) => a.localeCompare(b));
files.sort((a, b) => a.path.localeCompare(b.path));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Trace and JSONL sources are counted additively for the same session. Both providers ingest workflow-level trace totals and message-level JSONL totals. The shared root cause is that trace-based suppression is not in effect before JSONL aggregation runs.

  • src/lib/rollout.js#L7978-L7978: order kind: "trace" entries before kind: "jsonl" entries so tracedSessionIds is populated before the CodeBuddy JSONL branch checks it at Line 8369.
  • src/lib/rollout.js#L8727-L8797: add a persisted tracedSessionIds set to parseWorkbuddyIncremental, record trace.sessionId in the trace branch, and skip JSONL usage records for those sessions.
📍 Affects 1 file
  • src/lib/rollout.js#L7978-L7978 (this comment)
  • src/lib/rollout.js#L8727-L8797
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/rollout.js` at line 7978, The rollout aggregation currently counts
trace and JSONL totals for the same session. In src/lib/rollout.js lines
7978-7978, update the file sort to place kind: "trace" entries before kind:
"jsonl" entries. In parseWorkbuddyIncremental at src/lib/rollout.js lines
8727-8797, maintain a persisted tracedSessionIds set, add each trace.sessionId
in the trace branch, and skip JSONL usage records whose session IDs are already
tracked.

Comment thread src/lib/rollout.js
Comment thread src/lib/rollout.js
…ace sort order

Address CodeRabbit review feedback on PR xiufengsun#399:

1. Reasoning tokens: read from completion_tokens_details (not
   prompt_tokens_details) — verified on real data that reasoning lives
   in completion_details (999) not prompt_details (0). Also subtract
   reasoning from completionTokens to avoid double-counting (completion
   already includes reasoning).

2. WorkBuddy provider null pointer: use optional chaining (provider?.messageId)
   — provider (entry.providerData) may be null when only top-level usage is
   present (topUsage fallback path).

3. File sort order: sort trace files before jsonl files so tracedSessionIds
   is populated before jsonl parsing — prevents double-counting sessions
   that have both a trace file and a jsonl file.

4. Add trace-specific tests (test/codebuddy-traces.test.js):
   - trace discovery under traces/<pid>/trace_*.json
   - idempotent re-parse (traceId dedup)
   - trace + jsonl same session counted once (tracedSessionIds dedup)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/codebuddy-traces.test.js`:
- Around line 75-78: Update the environment cleanup block in the test teardown
to check saved.CODEBUDDY_HOME instead of saved.CODEX_HOME, preserving deletion
only when CODEBUDDY_HOME was originally undefined and otherwise restoring its
saved value.
- Around line 13-58: Update the traceJson and jsonlLine fixture builders to use
nullish fallback semantics for optional numeric fields, replacing || with ?? for
values such as totalTokens, miInput, miOutput, miCached, promptTokens,
completionTokens, cachedTokens, and reasoningTokens. Preserve explicit zero
values while retaining defaults for null or undefined inputs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: db922595-236e-499b-971a-aacae9e31ec8

📥 Commits

Reviewing files that changed from the base of the PR and between fe52757 and cfd0eac.

📒 Files selected for processing (2)
  • src/lib/rollout.js
  • test/codebuddy-traces.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/rollout.js

Comment thread test/codebuddy-traces.test.js
Comment thread test/codebuddy-traces.test.js
… in fixtures

Address remaining CodeRabbit review feedback:
- Fix saved.CODEX_HOME → saved.CODEBUDDY_HOME (copy-paste typo in cleanup)
- Replace || with ?? in test fixtures to preserve explicit zero values
….messageId

Adversarial review found two more provider dereferences without optional
chaining in the workbuddy branch:
- provider.model (model extraction)
- provider.messageId (dedup key, second access after the typeof check)

Both are safe in practice (guarded by typeof checks) but use provider?.
for consistency with the codebuddy branch and to be robust against
future refactors that might remove the guard.
…ting bug)

The workbuddy sqlite fallback branch (dbExists) computed
total_tokens = inputTokens only, dropping completionTokens + cacheRead +
cacheCreation + reasoningTokens. This is a pre-existing bug in the upstream
codebase (present in main), not introduced by this PR, but fixed here since
we're already touching the workbuddy collector.

@xiufengsun xiufengsun left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 6016d916e96f08c11a4b5bd30aae7aac0363849f merged onto current main 739e759950e4a470a3886feaee9868b5bf4ba5b6.

NO-MERGE: the targeted suite passes (234/234), but there are still deterministic accounting regressions not covered by it.

  1. Upgrading an existing CodeBuddy cursor double-counts historical JSONL usage. tracedSessionIds only suppresses JSONL lines parsed after this code lands (src/lib/rollout.js:8049-8057, 8373-8380). It never retracts the same session's totals already persisted in cursors.hourly. Reproduction: parse a JSONL session totaling 20,500 on the old parser, then parse a 10,000-token trace for that same session on this head. The queued bucket becomes 30,500; if the trace is authoritative it must be 10,000. This needs a safe one-time migration/rebuild, plus an upgrade regression test; a same-run test is not sufficient.

  2. WorkBuddy double-counts trace + JSONL even on a fresh install. Its trace branch (8760-8825) has no session-level suppression before the JSONL branch (8834+). A synthetic 10,000-token trace plus the same session's 10,000-token JSONL produces 20,000. Please apply equivalent source precedence and add a WorkBuddy overlap test.

  3. Older CodeBuddy installs can silently lose all new usage. resolveCodebuddyProjectFiles now disables extension logs unless TOKENTRACKER_CODEBUDDY_LOG_FALLBACK=1 (7909-7918), but those logs are the only source for versions with neither traces nor JSONL rawUsage. An undocumented opt-in is not a safe fallback. Source selection needs to happen automatically per session/source with cross-source deduplication, and needs a regression fixture for that older layout.

Please address all three paths and include upgrade-state fixtures, not only clean-cursor fixtures.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants