fix(claude): dedupe sidechain replayed usage#1155
Conversation
Track Claude Code isSidechain metadata on usage entries so /btw sidechain logs can be distinguished from parent session logs. When a sidechain file replays an existing assistant message with a new request id, dedupe it by message id without replacing the original parent usage. This prevents ccusage from counting the replayed cache-read history as fresh spend while still preserving distinct sidechain responses. Adds regression coverage for both entry loading and daily summary dedupe paths. Fixes #913.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
✅ Files skipped from review due to trivial changes (1)
📝 WalkthroughWalkthroughAdds ChangesSidechain usage tracking and deduplication
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
@coderabbitai review Initial push for #1155 is ready for review. |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
ccusage-guide | 04599a8 | Commit Preview URL Branch Preview URL |
May 25 2026, 07:06 PM |
|
Tip For best results, initiate chat on the files or code changes. PR Review:
|
| candidate | existing | returns | effect |
|---|---|---|---|
| parent | sidechain replay | true |
replace sidechain with parent ✓ |
| sidechain replay | parent | false |
keep parent ✓ |
loaded_entry_matches_sidechain_dedupe_key
entry.data.message.id.as_deref() == Some(message_id)
&& (candidate_is_sidechain || is_sidechain_usage_entry(&entry.data))The || condition ensures cross-request-id dedup only fires when at least one party is known to be a sidechain. This is appropriately conservative — for older logs where isSidechain was never written, neither side will be flagged and the fallback path will not trigger, avoiding false deduplication.
⚠️ Potential gap: DailyUsageLine::load hardcodes is_sidechain: None
// daily.rs, impl DailyUsageLine
DailyLoadedEntry {
...
is_sidechain: None, // <-- hardcoded
}read_daily_usage_file correctly propagates data.is_sidechain, but DailyUsageLine::load (a separate deserialization path) always emits None. If any daily-summary file written through the DailyUsageLine format can carry isSidechain: true, those entries will be treated as non-sidechain and the cross-request-id dedup won't fire for them.
Could you confirm whether sidechain metadata can appear in the DailyUsageLine format, or add a short comment explaining why None is safe here?
🔍 Minor: O(n) contains in push_deduped_index
fn push_deduped_index(deduped_indexes: &mut FxHashMap<u64, SmallIndexVec>, hash: u64, index: usize) {
let indexes = deduped_indexes.entry(hash).or_default();
if !indexes.contains(&index) { // linear scan
indexes.push(index);
}
}SmallIndexVec is presumably a SmallVec, so this is fine in practice since the number of entries sharing a message-only hash is small (typically ≤ 2: one parent + one sidechain replay). Not a bug — just flagging in case SmallIndexVec ever grows.
✅ What's well done
- Symmetric implementation between
mod.rs(per-entry) anddaily.rs(daily summary) — no risk of the two paths diverging. - Regression tests with realistic fixture data covering the exact scenario from
#913(parent entry survives with original token counts; sidechain replay is dropped; sidechain answer with a distinctmessage_idis kept separately). - Non-Claude adapters all updated to set
is_sidechain: None— struct exhaustiveness is maintained. - Conservative by default — absence of
isSidechainmetadata doesn't cause false deduplication.
Summary: One gap worth clarifying (DailyUsageLine hardcoded None), otherwise the implementation is clean and the tests are solid.
Propagate isSidechain from Claude AgentProgress lines into the daily summary path instead of dropping it during normalization. This closes the CodeRabbit review gap and keeps the sidechain dedupe behaviour consistent for both direct and AgentProgress daily records.
|
@coderabbitai review Pushed b5b27d2 to address your note: Daily AgentProgress normalization now propagates isSidechain, with a regression test covering that path. |
|
The propagation chain is now complete end-to-end:
The original concern is fully addressed. The implementation is solid — happy to approve. 🐇 ✨ ✅ Actions performedComments resolved. Approval is disabled; enable |
ccusage
@ccusage/ccusage-darwin-arm64
@ccusage/ccusage-darwin-x64
@ccusage/ccusage-linux-arm64
@ccusage/ccusage-linux-x64
@ccusage/ccusage-win32-arm64
@ccusage/ccusage-win32-x64
commit: |
Document Claude Code /btw side question logs, the isSidechain field used by ccusage, and issue #913 as the motivation for sidechain replay deduplication. Add short comments at the message-only fallback so the daily and full Claude loaders explain why sidechain duplicates can match by message ID when request IDs differ.
|
@coderabbitai review Pushed 04599a8 with Claude adapter README context for #913, the Claude Code /btw documentation link, and short comments explaining the sidechain message-ID fallback. |
|
README.md — The new "Sidechain entries" section is accurate and well-placed:
Inline comments ( The PR is in excellent shape across all three commits. Marking all comments resolved. ✅ Actions performedComments resolved. Approval is disabled; enable |
ccusage performance comparisonPR SHA: This compares the PR package against the configured base package on the same CI runner. Package runner startupExecution setup measures any pre-benchmark package materialization used by the execution benchmark. Bunx temp cache measures one
Cached bunx execution performanceRuns the same large fixture through Fixtures: Claude
Package runtime diagnosticsCompares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself. Fixtures: Claude
Committed fixture performanceCommitted small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage. Fixtures: Claude
Large real-world-shaped fixture performanceGenerated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures. Fixtures: Claude
Artifact size
Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees. |
ccusage performance comparisonPR SHA: This compares the Rust PR release binary against the configured base package on the same CI runner. Package runner startupExecution setup measures any pre-benchmark package materialization used by the execution benchmark. Bunx temp cache measures one
Cached bunx execution performanceRuns the same large fixture through Fixtures: Claude
Package runtime diagnosticsCompares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself. Fixtures: Claude
Committed fixture performanceCommitted small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage. Fixtures: Claude
Large real-world-shaped fixture performanceGenerated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures. Fixtures: Claude
Artifact size
Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees. |
ccusage performance comparisonPR SHA: This compares the Rust PR release binary against the configured base package on the same CI runner. Package runner startupExecution setup measures any pre-benchmark package materialization used by the execution benchmark. Bunx temp cache measures one
Cached bunx execution performanceRuns the same large fixture through Fixtures: Claude
Package runtime diagnosticsCompares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself. Fixtures: Claude
Committed fixture performanceCommitted small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage. Fixtures: Claude
Large real-world-shaped fixture performanceGenerated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures. Fixtures: Claude
Artifact size
Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees. |
ccusage performance comparisonPR SHA: This compares the PR package against the configured base package on the same CI runner. Package runner startupExecution setup measures any pre-benchmark package materialization used by the execution benchmark. Bunx temp cache measures one
Cached bunx execution performanceRuns the same large fixture through Fixtures: Claude
Package runtime diagnosticsCompares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself. Fixtures: Claude
Committed fixture performanceCommitted small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage. Fixtures: Claude
Large real-world-shaped fixture performanceGenerated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures. Fixtures: Claude
Artifact size
Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees. |
ccusage performance comparisonPR SHA: This compares the Rust PR release binary against the configured base package on the same CI runner. Package runner startupExecution setup measures any pre-benchmark package materialization used by the execution benchmark. Bunx temp cache measures one
Cached bunx execution performanceRuns the same large fixture through Fixtures: Claude
Package runtime diagnosticsCompares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself. Fixtures: Claude
Committed fixture performanceCommitted small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage. Fixtures: Claude
Large real-world-shaped fixture performanceGenerated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures. Fixtures: Claude
Artifact size
Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees. |
ccusage performance comparisonPR SHA: This compares the PR package against the configured base package on the same CI runner. Package runner startupExecution setup measures any pre-benchmark package materialization used by the execution benchmark. Bunx temp cache measures one
Cached bunx execution performanceRuns the same large fixture through Fixtures: Claude
Package runtime diagnosticsCompares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself. Fixtures: Claude
Committed fixture performanceCommitted small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage. Fixtures: Claude
Large real-world-shaped fixture performanceGenerated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures. Fixtures: Claude
Artifact size
Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees. |
Summary
Fixes Claude Code /btw sidechain overcounting by tracking isSidechain metadata and deduping replayed parent messages by message id. This prevents sidechain logs from re-counting the parent conversation history when Claude writes the same assistant message with a different request id, while still counting distinct sidechain responses.
Testing
Fixes #913.
@coderabbitai review
Summary by cubic
Fixes sidechain overcounting in Claude Code
/btwby trackingis_sidechainand preferring the parent when a sidechain replays a message with a newrequest_id, and documents the behavior. Applies to usage logs and daily AgentProgress. Fixes #913.is_sidechaintoUsageEntry; propagated through Claude loaders and daily AgentProgress; other adapters default toNone.message_id,request_id) and message-only; keep non-sidechain parent when a sidechain replays; otherwise replace by higher tokens/cost or presence of speed.Written for commit 04599a8. Summary will update on new commits. Review in cubic
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests